refactor: extract various constants
This commit is contained in:
@@ -10,13 +10,16 @@ import React, {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import type { CSSProperties, PointerEvent as ReactPointerEvent, RefObject } from 'react';
|
import type { CSSProperties, PointerEvent as ReactPointerEvent, RefObject } from 'react';
|
||||||
import { useSidebarContext } from '../sidebar/SidebarContext';
|
import { useSidebarContext } from '../sidebar/SidebarContext';
|
||||||
|
import {
|
||||||
type PanelKey = 'sidebar' | 'detail';
|
DEFAULT_DETAIL_WIDTH,
|
||||||
|
DEFAULT_SIDEBAR_WIDTH,
|
||||||
interface PanelLimits {
|
MINIMAL_FREE_RATIO,
|
||||||
maxRatio: number;
|
MINIMUM_MAIN_CONTENT_WIDTH,
|
||||||
minPx: number;
|
PANEL_LIMITS,
|
||||||
}
|
PANEL_STORAGE_KEYS,
|
||||||
|
SIDEBAR_SOLO_THRESHOLD,
|
||||||
|
type PanelKey,
|
||||||
|
} from '../constants/layout';
|
||||||
|
|
||||||
interface SetPanelWidthOptions {
|
interface SetPanelWidthOptions {
|
||||||
commit?: boolean;
|
commit?: boolean;
|
||||||
@@ -50,29 +53,6 @@ type PanelResizeBindings = {
|
|||||||
|
|
||||||
const PanelManagerContext = createContext<PanelManagerContextValue | null>(null);
|
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 clampPanelWidth = (panel: PanelKey, value: number): number => {
|
||||||
const numeric = Number(value);
|
const numeric = Number(value);
|
||||||
const limits = PANEL_LIMITS[panel];
|
const limits = PANEL_LIMITS[panel];
|
||||||
@@ -86,7 +66,7 @@ const clampPanelWidth = (panel: PanelKey, value: number): number => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const readStoredWidth = (panel: PanelKey, fallback: 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) {
|
if (!raw) {
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
@@ -95,7 +75,7 @@ const readStoredWidth = (panel: PanelKey, fallback: number): number => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const persistWidth = (panel: PanelKey, value: number): void => {
|
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 => {
|
const applyPanelWidthToRoot = (panel: PanelKey, width: number, active: boolean): void => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useContext, useEffect, useMemo, useReducer } from 'react';
|
|||||||
import { clearAuthToken, setAuthToken, setAuthRefreshHandlers } from '../lib/apiClient';
|
import { clearAuthToken, setAuthToken, setAuthRefreshHandlers } from '../lib/apiClient';
|
||||||
import { ApiProvider } from './ApiContext';
|
import { ApiProvider } from './ApiContext';
|
||||||
import { listTenants } from '../lib/apiClient';
|
import { listTenants } from '../lib/apiClient';
|
||||||
|
import { STORED_TOKEN_KEY } from '../constants/app';
|
||||||
|
|
||||||
type Tenant = Record<string, unknown> | null;
|
type Tenant = Record<string, unknown> | null;
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ type AppAction =
|
|||||||
|
|
||||||
const storage = window.sessionStorage;
|
const storage = window.sessionStorage;
|
||||||
|
|
||||||
const STORED_TOKEN = storage?.getItem('papercrate_token') ?? '';
|
const storedToken = storage?.getItem(STORED_TOKEN_KEY) ?? '';
|
||||||
let STORED_TENANT: Tenant = null;
|
let STORED_TENANT: Tenant = null;
|
||||||
|
|
||||||
if (storage) {
|
if (storage) {
|
||||||
@@ -61,13 +62,13 @@ if (storage) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (STORED_TOKEN) {
|
if (storedToken) {
|
||||||
setAuthToken(STORED_TOKEN);
|
setAuthToken(storedToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialAppState: AppState = {
|
const initialAppState: AppState = {
|
||||||
status: STORED_TOKEN ? 'authenticated' : 'logged-out',
|
status: storedToken ? 'authenticated' : 'logged-out',
|
||||||
token: STORED_TOKEN,
|
token: storedToken,
|
||||||
error: null,
|
error: null,
|
||||||
isRefreshing: false,
|
isRefreshing: false,
|
||||||
tenantSelection: null,
|
tenantSelection: null,
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import type { DocumentId, FolderId } from '../types/identifiers';
|
import type { DocumentId, FolderId } from '../types/identifiers';
|
||||||
|
import { ENTRY_KEY_SEPARATOR } from '../constants/app';
|
||||||
|
|
||||||
// Entry key utilities for workspace selection
|
// Entry key utilities for workspace selection
|
||||||
// Entry keys are strings in the format "document:id" or "folder:id"
|
// Entry keys are strings in the format "document:id" or "folder:id"
|
||||||
|
|
||||||
const ENTRY_KEY_SEPARATOR = ':';
|
|
||||||
|
|
||||||
// Create entry key strings
|
// Create entry key strings
|
||||||
export const createDocumentEntryKey = (documentId: DocumentId): string =>
|
export const createDocumentEntryKey = (documentId: DocumentId): string =>
|
||||||
`document${ENTRY_KEY_SEPARATOR}${documentId}`;
|
`document${ENTRY_KEY_SEPARATOR}${documentId}`;
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import {
|
|||||||
DEFAULT_SORT_FIELD,
|
DEFAULT_SORT_FIELD,
|
||||||
SORT_FIELD_VALUES,
|
SORT_FIELD_VALUES,
|
||||||
} from './workspaceUtils';
|
} from './workspaceUtils';
|
||||||
|
import {
|
||||||
const VIEW_MODE_STORAGE_KEY = 'papercrate_view_mode';
|
INCLUDE_DESCENDANTS_STORAGE_KEY,
|
||||||
const SORT_FIELD_STORAGE_KEY = 'papercrate_sort_field';
|
SORT_DIRECTION_STORAGE_KEY,
|
||||||
const SORT_DIRECTION_STORAGE_KEY = 'papercrate_sort_direction';
|
SORT_FIELD_STORAGE_KEY,
|
||||||
const INCLUDE_DESCENDANTS_STORAGE_KEY = 'papercrate_include_descendants';
|
VIEW_MODE_STORAGE_KEY,
|
||||||
|
} from '../constants/workspace';
|
||||||
|
|
||||||
const readSessionStorage = (key: string): string | null => {
|
const readSessionStorage = (key: string): string | null => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import TagsPanel from '../tags/TagsPanel';
|
|||||||
import CorrespondentsPanel, { CorrespondentsPanelProps } from '../correspondents/CorrespondentsPanel';
|
import CorrespondentsPanel, { CorrespondentsPanelProps } from '../correspondents/CorrespondentsPanel';
|
||||||
import PanelHeader from '../ui/PanelHeader';
|
import PanelHeader from '../ui/PanelHeader';
|
||||||
import { CloseIcon } from '../ui/icons';
|
import { CloseIcon } from '../ui/icons';
|
||||||
|
import { CORRESPONDENTS_MODAL, TAGS_MODAL } from '../constants/app';
|
||||||
const TAGS_MODAL = 'tags';
|
|
||||||
const CORRESPONDENTS_MODAL = 'correspondents';
|
|
||||||
|
|
||||||
interface TagRecord {
|
interface TagRecord {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
export const DEFAULT_FOLDER_NAME = 'Documents';
|
import {
|
||||||
export const DEFAULT_SORT_FIELD = 'title';
|
DEFAULT_FOLDER_NAME,
|
||||||
export const DEFAULT_SORT_DIRECTION = 'asc';
|
DEFAULT_SORT_DIRECTION,
|
||||||
export const SORT_FIELD_VALUES = ['title', 'issued_at', 'created_at', 'updated_at'];
|
DEFAULT_SORT_FIELD,
|
||||||
export const TAG_FILTER_UNTAGGED = '__UNTAGGED__';
|
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) =>
|
export const hasFiles = (event) =>
|
||||||
Array.from(event.dataTransfer?.types || []).includes('Files');
|
Array.from(event.dataTransfer?.types || []).includes('Files');
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export const HEX_COLOR_PATTERN = /^#?([0-9a-fA-F]{6})$/;
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
}, {});
|
||||||
@@ -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',
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
@@ -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,
|
||||||
|
];
|
||||||
@@ -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)';
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -25,6 +25,7 @@ import useDeskPointer from './pointer/useDeskPointer';
|
|||||||
import useDeskTagInteractions from './tags/useDeskTagInteractions';
|
import useDeskTagInteractions from './tags/useDeskTagInteractions';
|
||||||
import DesktopDocumentCard from './DesktopDocumentCard';
|
import DesktopDocumentCard from './DesktopDocumentCard';
|
||||||
import usePreviewMetadata from './hooks/usePreviewMetadata';
|
import usePreviewMetadata from './hooks/usePreviewMetadata';
|
||||||
|
import { DEBUG_DRAG, DEBUG_FOCUS } from '../constants/desktop';
|
||||||
import '../styles/workspace/workspace-layout.css';
|
import '../styles/workspace/workspace-layout.css';
|
||||||
import '../styles/workspace/workspace-items.css';
|
import '../styles/workspace/workspace-items.css';
|
||||||
import '../styles/workspace/workspace-cards.css';
|
import '../styles/workspace/workspace-cards.css';
|
||||||
@@ -186,8 +187,6 @@ interface DesktopWorkspaceViewProps {
|
|||||||
markLayoutDirty: () => void;
|
markLayoutDirty: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEBUG_DRAG = false;
|
|
||||||
const DEBUG_FOCUS = false;
|
|
||||||
const defaultGetDocumentAsset: GetAsset = () => null;
|
const defaultGetDocumentAsset: GetAsset = () => null;
|
||||||
|
|
||||||
const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
const DesktopWorkspace: React.FC<DesktopWorkspaceProps> = ({
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import type { DocumentId } from '../types/identifiers';
|
import type { DocumentId } from '../types/identifiers';
|
||||||
|
import { DB_NAME, DB_VERSION, LAYOUT_STORE } from '../constants/desktop';
|
||||||
type TenantId = import('../types/identifiers').TenantId;
|
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 currentDbPromise: { value: Promise<IDBDatabase | null> | null } = { value: null };
|
||||||
|
|
||||||
const openDatabase = (): Promise<IDBDatabase> => {
|
const openDatabase = (): Promise<IDBDatabase> => {
|
||||||
|
|||||||
@@ -1,28 +1,18 @@
|
|||||||
import { safeInvoke } from '../events';
|
import { safeInvoke } from '../events';
|
||||||
import type { DocumentId } from '../../types/identifiers';
|
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 = {
|
export { CLICK_ACTIONS, DRAG_ACTIONS, STACK_HIT_EPSILON, POINTER_DRAG_THRESHOLD_SQUARED, LONG_PRESS_DURATION_MS };
|
||||||
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 type ClickAction = (typeof CLICK_ACTIONS)[keyof typeof CLICK_ACTIONS];
|
export type ClickAction = (typeof CLICK_ACTIONS)[keyof typeof CLICK_ACTIONS];
|
||||||
export type DragAction = (typeof DRAG_ACTIONS)[keyof typeof DRAG_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;
|
export const withinThreshold = (dx: number, dy: number, thresholdSquared: number): boolean => (dx * dx + dy * dy) <= thresholdSquared;
|
||||||
|
|
||||||
interface PointerIntentArgs {
|
interface PointerIntentArgs {
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import {
|
|||||||
parseTagTransferPayload,
|
parseTagTransferPayload,
|
||||||
writeTagTransferData,
|
writeTagTransferData,
|
||||||
} from '../../documents/tagTransfer';
|
} from '../../documents/tagTransfer';
|
||||||
|
import { TAG_REMOVE_DISTANCE } from '../../constants/desktop';
|
||||||
const TAG_REMOVE_DISTANCE = 160;
|
|
||||||
|
|
||||||
const createDragPreview = (node, clientX, clientY) => {
|
const createDragPreview = (node, clientX, clientY) => {
|
||||||
if (!(node instanceof HTMLElement)) {
|
if (!(node instanceof HTMLElement)) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
applyDomTransform,
|
applyDomTransform,
|
||||||
type WorkspaceEngine,
|
type WorkspaceEngine,
|
||||||
} from './workspaceEngine';
|
} from './workspaceEngine';
|
||||||
|
import { DRAG_HYSTERESIS_SQUARED, EDGE_COLLISION_THRESHOLD } from '../constants/desktop';
|
||||||
import type { DocumentId, Identifier } from '../types/identifiers';
|
import type { DocumentId, Identifier } from '../types/identifiers';
|
||||||
|
|
||||||
interface DocumentLike {
|
interface DocumentLike {
|
||||||
@@ -168,10 +169,6 @@ interface DragTapMetadata {
|
|||||||
docTitle: string;
|
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 getDocumentPageCount = (doc?: DocumentLike | null): number | null => {
|
||||||
const raw = doc?.current_version?.metadata?.page_count ?? doc?.metadata?.page_count;
|
const raw = doc?.current_version?.metadata?.page_count ?? doc?.metadata?.page_count;
|
||||||
if (raw == null) {
|
if (raw == null) {
|
||||||
|
|||||||
@@ -1,8 +1,44 @@
|
|||||||
import { clamp, formatTransform } from '../utils/math';
|
import { clamp, formatTransform } from '../utils/math';
|
||||||
import { fetchLayoutRecords, upsertLayoutRecords } from './db';
|
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';
|
import type { DocumentId } from '../types/identifiers';
|
||||||
type TenantId = import('../types/identifiers').TenantId;
|
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 {
|
interface Point {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
@@ -128,24 +164,6 @@ type UseSyncExternalStoreHook = <State>(
|
|||||||
getServerSnapshot: () => State,
|
getServerSnapshot: () => State,
|
||||||
) => 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 => {
|
const getMassScale = (massGrams?: number): number => {
|
||||||
if (!Number.isFinite(massGrams) || Number(massGrams) <= 0) {
|
if (!Number.isFinite(massGrams) || Number(massGrams) <= 0) {
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { NBSP } from '../constants/ui';
|
||||||
const NBSP = String.fromCharCode(160);
|
|
||||||
|
|
||||||
export interface CorrespondentLinkEntry {
|
export interface CorrespondentLinkEntry {
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
resolveDocumentAssetUrl,
|
resolveDocumentAssetUrl,
|
||||||
resolveAssetUrl,
|
resolveAssetUrl,
|
||||||
} from '../asset_manager';
|
} from '../asset_manager';
|
||||||
|
import { DEFAULT_THUMBNAIL_SIZE } from '../constants/documents';
|
||||||
import type {
|
import type {
|
||||||
DocumentLike as AssetManagerDocumentLike,
|
DocumentLike as AssetManagerDocumentLike,
|
||||||
AssetLike as AssetManagerAssetLike,
|
AssetLike as AssetManagerAssetLike,
|
||||||
@@ -12,8 +13,6 @@ import type {
|
|||||||
GetAsset as AssetManagerGetAsset,
|
GetAsset as AssetManagerGetAsset,
|
||||||
} from '../asset_manager';
|
} from '../asset_manager';
|
||||||
|
|
||||||
const DEFAULT_THUMBNAIL_SIZE = 48;
|
|
||||||
|
|
||||||
// Detect when an element becomes visible within a scroll container so we can delay loading.
|
// Detect when an element becomes visible within a scroll container so we can delay loading.
|
||||||
const useLazyVisibility = (
|
const useLazyVisibility = (
|
||||||
rootRef: MutableRefObject<Element | null> | null,
|
rootRef: MutableRefObject<Element | null> | null,
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ import SelectionAssignmentMenu, { SelectionAssignmentMenuItem } from './Selectio
|
|||||||
import SelectionSummary from './SelectionSummary';
|
import SelectionSummary from './SelectionSummary';
|
||||||
import { useAppState } from '../app/appState';
|
import { useAppState } from '../app/appState';
|
||||||
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
|
import { useWorkspaceSelectionContext } from '../app/WorkspaceSelectionContext';
|
||||||
|
import { DEFAULT_FOLDER_NAME } from '../constants/workspace';
|
||||||
import type { DocumentId } from '../types/identifiers';
|
import type { DocumentId } from '../types/identifiers';
|
||||||
|
|
||||||
const ROOT_FOLDER_LABEL = 'Documents';
|
|
||||||
|
|
||||||
type NullableDocumentId = DocumentId | null;
|
type NullableDocumentId = DocumentId | null;
|
||||||
|
|
||||||
type SelectedIdList = NullableDocumentId[] | 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' }));
|
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 = (
|
const buildTagAssignments = (
|
||||||
|
|||||||
@@ -18,10 +18,9 @@ import DocumentsPanelHeader, {
|
|||||||
import { SelectionFloatingPanel } from '../SelectionFloatingActions';
|
import { SelectionFloatingPanel } from '../SelectionFloatingActions';
|
||||||
import { createDocumentsTableHeaderActions } from './DocumentsToolbar';
|
import { createDocumentsTableHeaderActions } from './DocumentsToolbar';
|
||||||
import { useDocumentsFilter } from '../context/DocumentsFilterContext';
|
import { useDocumentsFilter } from '../context/DocumentsFilterContext';
|
||||||
|
import { DEFAULT_GRID_ICON_SIZE } from '../../constants/documents';
|
||||||
import type { Identifier } from '../../types/identifiers';
|
import type { Identifier } from '../../types/identifiers';
|
||||||
|
|
||||||
const DEFAULT_GRID_ICON_SIZE = 144;
|
|
||||||
|
|
||||||
const EntryType = {
|
const EntryType = {
|
||||||
folder: 'folder',
|
folder: 'folder',
|
||||||
document: 'document',
|
document: 'document',
|
||||||
|
|||||||
@@ -1,19 +1,7 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { SORT_LABEL_LOOKUP, SORT_OPTIONS } from '../../constants/documents';
|
||||||
import QuickAddMenu from '../../ui/QuickAddMenu';
|
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 {
|
interface SortFieldQuickMenuProps {
|
||||||
sortField: string;
|
sortField: string;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { DocumentId, TagId } from '../types/identifiers';
|
import type { DocumentId, TagId } from '../types/identifiers';
|
||||||
|
import { TAG_MIME_TYPES, TAG_TEXT_MIME_TYPE } from '../constants/documents';
|
||||||
const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag'];
|
|
||||||
const TAG_TEXT_MIME_TYPE = 'text/plain';
|
|
||||||
|
|
||||||
interface TagPayload {
|
interface TagPayload {
|
||||||
id: TagId;
|
id: TagId;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { JSX } from 'react';
|
|||||||
import DocumentInfoPanel from '../documents/DocumentInfoPanel';
|
import DocumentInfoPanel from '../documents/DocumentInfoPanel';
|
||||||
import { DownloadIcon } from '../ui/icons';
|
import { DownloadIcon } from '../ui/icons';
|
||||||
import PdfViewer from './PdfViewer';
|
import PdfViewer from './PdfViewer';
|
||||||
|
import { AUDIO_EXTENSIONS, VIDEO_EXTENSIONS } from '../constants/preview';
|
||||||
|
|
||||||
interface DocumentLike {
|
interface DocumentLike {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -48,29 +49,6 @@ interface DocumentViewerLayoutProps {
|
|||||||
layoutMode?: LayoutMode;
|
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) => {
|
const getFileExtension = (filename?: string | null) => {
|
||||||
if (!filename) {
|
if (!filename) {
|
||||||
return '';
|
return '';
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ import type {
|
|||||||
PDFDocumentProxy,
|
PDFDocumentProxy,
|
||||||
RenderTask,
|
RenderTask,
|
||||||
} from 'pdfjs-dist/types/src/display/api';
|
} 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(
|
GlobalWorkerOptions.workerSrc = new URL(
|
||||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||||
import.meta.url,
|
import.meta.url,
|
||||||
@@ -45,12 +52,6 @@ interface RenderQueueRequest {
|
|||||||
cancel: () => void;
|
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 getAvailableViewportSize = (viewportNode: Element, stackNode: Element) => {
|
||||||
const viewportStyle = window.getComputedStyle(viewportNode);
|
const viewportStyle = window.getComputedStyle(viewportNode);
|
||||||
const viewportPaddingX = parseFloat(viewportStyle.paddingLeft || '0')
|
const viewportPaddingX = parseFloat(viewportStyle.paddingLeft || '0')
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { MutableRefObject, useLayoutEffect, useState } from 'react';
|
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;
|
export { DOCUMENT_VIEWER_PORTRAIT_HEIGHT_RATIO };
|
||||||
const PORTRAIT_RATIO_STYLE_ID = 'document-viewer-portrait-ratio-style';
|
|
||||||
|
|
||||||
const ensurePortraitRatioStyle = () => {
|
const ensurePortraitRatioStyle = () => {
|
||||||
const cssValue = String(DOCUMENT_VIEWER_PORTRAIT_HEIGHT_RATIO);
|
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);
|
const computeStackedLayoutBreakpoint = () => Math.max(window.innerWidth / 2, MIN_STACKED_BREAKPOINT);
|
||||||
|
|
||||||
export const useViewerLayoutMode = (
|
export const useViewerLayoutMode = (
|
||||||
|
|||||||
@@ -489,10 +489,4 @@ const ApiTokensSection = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const API_TOKENS_SECTION = {
|
|
||||||
id: 'apiTokens',
|
|
||||||
label: 'API tokens',
|
|
||||||
component: ApiTokensSection,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ApiTokensSection;
|
export default ApiTokensSection;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import React, {
|
|||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import type { SettingsSectionConfig } from '../SettingsModal';
|
|
||||||
import { IconX } from '../../ui/icons';
|
import { IconX } from '../../ui/icons';
|
||||||
import CapabilityDropdown, { CapabilityDropdownOption } from '../components/CapabilityDropdown';
|
import CapabilityDropdown, { CapabilityDropdownOption } from '../components/CapabilityDropdown';
|
||||||
import type { CapabilitySetId, CapabilityValue } from '../../types/identifiers';
|
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;
|
export default CapabilitySetsSection;
|
||||||
|
|||||||
@@ -172,10 +172,4 @@ const PasskeysSection = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PASSKEYS_SECTION = {
|
|
||||||
id: 'passkeys',
|
|
||||||
label: 'Passkeys',
|
|
||||||
component: PasskeysSection,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PasskeysSection;
|
export default PasskeysSection;
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import PasskeysSection, { PASSKEYS_SECTION } from './PasskeysSection';
|
import PasskeysSection from './PasskeysSection';
|
||||||
import ApiTokensSection, { API_TOKENS_SECTION } from './ApiTokensSection';
|
import ApiTokensSection from './ApiTokensSection';
|
||||||
import CapabilitySetsSection, { CAPABILITY_SETS_SECTION } from './CapabilitySetsSection';
|
import CapabilitySetsSection from './CapabilitySetsSection';
|
||||||
|
import {
|
||||||
export const DEFAULT_SETTINGS_SECTIONS = [
|
|
||||||
PASSKEYS_SECTION,
|
|
||||||
API_TOKENS_SECTION,
|
API_TOKENS_SECTION,
|
||||||
CAPABILITY_SETS_SECTION,
|
CAPABILITY_SETS_SECTION,
|
||||||
];
|
DEFAULT_SETTINGS_SECTIONS,
|
||||||
|
PASSKEYS_SECTION,
|
||||||
|
} from '../../constants/settings';
|
||||||
|
|
||||||
export {
|
export { PasskeysSection, ApiTokensSection, CapabilitySetsSection };
|
||||||
PasskeysSection,
|
export { PASSKEYS_SECTION, API_TOKENS_SECTION, CAPABILITY_SETS_SECTION, DEFAULT_SETTINGS_SECTIONS };
|
||||||
ApiTokensSection,
|
|
||||||
CapabilitySetsSection,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { useSidebarContext } from './SidebarContext';
|
|||||||
import { usePanelManager, usePanelResizeBindings } from '../app/PanelManagerContext';
|
import { usePanelManager, usePanelResizeBindings } from '../app/PanelManagerContext';
|
||||||
import type { StatusMessage } from '../hooks/documents/store/useDocumentsStore';
|
import type { StatusMessage } from '../hooks/documents/store/useDocumentsStore';
|
||||||
import type { Identifier } from '../types/identifiers';
|
import type { Identifier } from '../types/identifiers';
|
||||||
|
import { THEME_MODE_LABELS, THEME_MODES } from '../constants/sidebar';
|
||||||
|
|
||||||
interface CommunityLink {
|
interface CommunityLink {
|
||||||
label: string;
|
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 {
|
interface FloatingMenuControls {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
toggle: () => void;
|
toggle: () => void;
|
||||||
|
|||||||
@@ -6,6 +6,16 @@ import React, {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
} from 'react';
|
} 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 {
|
interface SidebarContextValue {
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
@@ -25,20 +35,10 @@ interface SidebarContextValue {
|
|||||||
themeModes: ThemeMode[];
|
themeModes: ThemeMode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type ThemeMode = 'system' | 'light' | 'dark';
|
type ThemeMode = (typeof THEME_MODES)[number];
|
||||||
|
|
||||||
const SIDEBAR_COLLAPSE_STORAGE_KEY = 'papercrate_sidebar_collapsed';
|
|
||||||
|
|
||||||
const SidebarContext = createContext<SidebarContextValue | null>(null);
|
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 loadInitialThemeSettings = () => {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
neutralHue: DEFAULT_NEUTRAL_HUE,
|
neutralHue: DEFAULT_NEUTRAL_HUE,
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ import React, {
|
|||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import useFloatingMenu from './useFloatingMenu';
|
import useFloatingMenu from './useFloatingMenu';
|
||||||
|
import {
|
||||||
|
ELLIPSIS,
|
||||||
|
WIDTH_BUFFER_RATIO,
|
||||||
|
WIDTH_CHANGE_TOLERANCE,
|
||||||
|
WIDTH_TOLERANCE,
|
||||||
|
} from '../constants/ui';
|
||||||
|
|
||||||
const normalizeEntries = (entries) =>
|
const normalizeEntries = (entries) =>
|
||||||
(Array.isArray(entries) ? entries : [])
|
(Array.isArray(entries) ? entries : [])
|
||||||
@@ -22,11 +28,6 @@ const normalizeEntries = (entries) =>
|
|||||||
})
|
})
|
||||||
.filter(Boolean);
|
.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 = ({
|
const BreadcrumbTrail = ({
|
||||||
entries = [],
|
entries = [],
|
||||||
className = '',
|
className = '',
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import type { MutableRefObject, CSSProperties } from 'react';
|
import type { MutableRefObject, CSSProperties } from 'react';
|
||||||
import { clamp } from '../utils/math';
|
import { clamp } from '../utils/math';
|
||||||
|
import { DEFAULT_VIEWPORT_MARGIN } from '../constants/ui';
|
||||||
const DEFAULT_VIEWPORT_MARGIN = 8;
|
|
||||||
|
|
||||||
type PositionStrategy = 'fixed' | 'absolute' | (string & {});
|
type PositionStrategy = 'fixed' | 'absolute' | (string & {});
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
const clamp01 = (value: number): number => Math.min(1, Math.max(0, value));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user