Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 91 additions & 8 deletions apps/diffshub/components/DiffsHubHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,8 @@ import { type ColorMode } from '@pierre/theming';
import Link from 'next/link';
import {
type CSSProperties,
type Dispatch,
memo,
type SetStateAction,
useCallback,
useLayoutEffect,
useMemo,
useRef,
Expand All @@ -44,6 +43,11 @@ import {
import { Switch } from '@/components/Switch';
import { docsThemeCatalog } from '@/components/themeCatalog';
import { cn } from '@/lib/cn';
import {
DIFFS_HUB_CODE_FONT_OPTIONS,
type DiffsHubCodeFont,
isDiffsHubDefaultCodeFont,
} from '@/lib/displayPreferences';
import { diffshubChromeMapping } from '@/lib/theme/diffshubChromeMapping';
import { getDropdownThemeStyle } from '@/lib/theme/dropdownChromeStyle';

Expand All @@ -55,6 +59,7 @@ const SETTING_ROW_CLASS =

interface HeaderProps {
className?: string;
codeFont: DiffsHubCodeFont;
collapseMode: 'expanded' | 'collapsed';
colorMode: ColorMode;
darkThemeName: DarkThemeName;
Expand All @@ -68,19 +73,21 @@ interface HeaderProps {
overflow: 'wrap' | 'scroll';
onToggleCollapseMode(): void;
onToggleFileTreeOverlay(): void;
setCodeFont(value: DiffsHubCodeFont): void;
setColorMode(mode: ColorMode): void;
setDarkThemeName(name: DarkThemeName): void;
setDiffIndicators: Dispatch<SetStateAction<DiffIndicators>>;
setDiffStyle: Dispatch<SetStateAction<'split' | 'unified'>>;
setDiffIndicators(value: DiffIndicators): void;
setDiffStyle(value: 'split' | 'unified'): void;
setLightThemeName(name: LightThemeName): void;
setLineNumbers: Dispatch<SetStateAction<boolean>>;
setOverflow: Dispatch<SetStateAction<'wrap' | 'scroll'>>;
setShowBackgrounds: Dispatch<SetStateAction<boolean>>;
setLineNumbers(value: boolean): void;
setOverflow(value: 'wrap' | 'scroll'): void;
setShowBackgrounds(value: boolean): void;
showBackgrounds: boolean;
}

export const DiffsHubHeader = memo(function DiffsHubHeader({
className,
codeFont,
collapseMode,
colorMode,
darkThemeName,
Expand All @@ -94,6 +101,7 @@ export const DiffsHubHeader = memo(function DiffsHubHeader({
overflow,
onToggleCollapseMode,
onToggleFileTreeOverlay,
setCodeFont,
setColorMode,
setDarkThemeName,
setDiffIndicators,
Expand All @@ -105,6 +113,20 @@ export const DiffsHubHeader = memo(function DiffsHubHeader({
showBackgrounds,
}: HeaderProps) {
const [currentUrl, setCurrentUrl] = useState(initialUrl);
const codeFontSelection = codeFont.kind === 'default' ? 'default' : 'custom';
const customCodeFontFamily =
codeFont.kind === 'default' ? '' : codeFont.input;
const focusCustomCodeFontInput = useCallback(
(input: HTMLInputElement | null) => {
if (input == null || document.activeElement === input) {
return;
}

input.focus({ preventScroll: true });
input.setSelectionRange(input.value.length, input.value.length);
},
[]
);
// Only show the external-link button when the input still reflects the
// committed URL — otherwise we'd be pointing at a draft the user is editing.
const showExternalLink = currentUrl === initialUrl;
Expand Down Expand Up @@ -238,7 +260,7 @@ export const DiffsHubHeader = memo(function DiffsHubHeader({
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-58 p-2"
className="w-72 p-2"
style={dropdownThemeStyle}
>
<DropdownMenuItem
Expand Down Expand Up @@ -279,6 +301,67 @@ export const DiffsHubHeader = memo(function DiffsHubHeader({
/>
</label>
</DropdownMenuItem>
<div className="flex flex-col items-stretch gap-2 px-2 py-1.5 text-sm">
<div className="flex w-full items-center justify-between gap-4">
<span className="min-w-0 whitespace-nowrap">Code font</span>
<ButtonGroup
className="ml-auto w-40"
value={codeFontSelection}
onValueChange={(value) => {
if (isDiffsHubDefaultCodeFont(value)) {
setCodeFont({
kind: 'default',
});
return;
}

if (value === 'custom') {
setCodeFont(
codeFont.kind !== 'default'
? codeFont
: {
input: '',
kind: 'custom',
}
);
}
}}
>
{DIFFS_HUB_CODE_FONT_OPTIONS.map((option) => (
<ButtonGroupItem
key={option.value}
value={option.value}
className="h-7 flex-1 px-2"
>
{option.label}
</ButtonGroupItem>
))}
<ButtonGroupItem
value="custom"
className="h-7 flex-1 px-2"
>
Custom
</ButtonGroupItem>
</ButtonGroup>
</div>
{codeFont.kind !== 'default' && (
<input
ref={focusCustomCodeFontInput}
className="border-input bg-background text-foreground placeholder:text-muted-foreground focus-visible:ring-ring/50 h-8 w-full rounded-md border px-2 text-sm outline-none focus-visible:ring-2"
enterKeyHint="done"
placeholder="JetBrains Mono"
spellCheck={false}
value={customCodeFontFamily}
onChange={({ currentTarget }) => {
setCodeFont({
input: currentTarget.value,
kind: 'custom',
});
}}
onKeyDown={(event) => event.stopPropagation()}
/>
)}
</div>
<DropdownMenuItem
className="w-full px-2 focus:bg-transparent"
onSelect={(e) => e.preventDefault()}
Expand Down
21 changes: 19 additions & 2 deletions apps/diffshub/components/DiffsHubViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ import {
} from '@pierre/diffs';
import { type CodeViewHandle, useStableCallback } from '@pierre/diffs/react';
import { IconChevronSm } from '@pierre/icons';
import { memo, type RefObject, useMemo, useRef, useState } from 'react';
import {
type CSSProperties,
memo,
type RefObject,
useMemo,
useRef,
useState,
} from 'react';

import { DraftAnnotation } from './DraftAnnotation';
import { ExampleAnnotation } from './ExampleAnnotation';
Expand Down Expand Up @@ -63,6 +70,7 @@ interface ActiveDraftComment {

interface DiffsHubViewerProps {
className?: string;
codeFontFamily: string;
diffStyle: 'split' | 'unified';
onCommentDeleted(comment: DiffsHubDeletedCommentEvent): void;
onCommentSaved(comment: DiffsHubSavedCommentEvent): void;
Expand All @@ -80,6 +88,7 @@ interface DiffsHubViewerProps {

export const DiffsHubViewer = memo(function DiffsHubViewer({
className,
codeFontFamily,
diffStyle,
onCommentDeleted,
onCommentSaved,
Expand Down Expand Up @@ -107,6 +116,14 @@ export const DiffsHubViewer = memo(function DiffsHubViewer({
() => buildAnnotationThemeStyle(themeChromeStyle),
[themeChromeStyle]
);
const viewerStyle = useMemo(
() =>
({
...(annotationThemeStyle ?? {}),
'--diffs-font-family': codeFontFamily,
}) satisfies CSSProperties & { '--diffs-font-family': string },
[annotationThemeStyle, codeFontFamily]
);

const handleSetSelection = useStableCallback(
(selection: CodeViewLineSelection | null) => {
Expand Down Expand Up @@ -471,7 +488,7 @@ export const DiffsHubViewer = memo(function DiffsHubViewer({
'cv-scrollbar relative h-full min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-clip overscroll-contain border-b border-border w-full [contain:strict] [overflow-anchor:none] [will-change:scroll-position] md:border-b-0 [&_diffs-container]:overflow-clip [&_diffs-container]:[contain:layout_paint_style] [&_diffs-container]:shadow-[0_-1px_0_var(--diffshub-diff-separator,var(--color-border-opaque)),0_1px_0_var(--diffshub-diff-separator,var(--color-border-opaque))]'
)}
options={options}
style={annotationThemeStyle}
style={viewerStyle}
selectedLines={selectedLines}
onSelectedLinesChange={handleSetSelection}
renderAnnotation={renderCommentAnnotation}
Expand Down
51 changes: 39 additions & 12 deletions apps/diffshub/components/ReviewUI.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client';

import { type DiffIndicators } from '@pierre/diffs';
import { type CodeViewHandle, useWorkerPool } from '@pierre/diffs/react';
import { type ColorMode } from '@pierre/theming';
import { useThemeController } from '@pierre/theming/react';
Expand All @@ -24,6 +23,10 @@ import {
themeController,
} from '@/components/themeController';
import { preloadAvatars } from '@/lib/annotation';
import {
getDiffsHubCodeFontFamily,
useDiffsHubDisplayPreferences,
} from '@/lib/displayPreferences';
import { removeSavedCommentSidebarEntry } from '@/lib/removeSavedCommentSidebarEntry';
import type { DarkThemeName, LightThemeName } from '@/lib/themeNames';
import type {
Expand Down Expand Up @@ -54,15 +57,31 @@ function ReviewUIInner({ domain, initialUrl, path }: ReviewUIProps) {
useEffect(preloadAvatars, []);

const isWorkerPoolReadyOrDisable = useIsWorkerPoolReadyOrDisabled();
const [diffStyle, setDiffStyle] = useState<'split' | 'unified'>('split');
const [collapseMode, setCollapseMode] = useState<'expanded' | 'collapsed'>(
'expanded'
);
const [fileTreeOverlayOpen, setFileTreeOverlayOpen] = useState(false);
const [overflow, setOverflow] = useState<'wrap' | 'scroll'>('scroll');
const [showBackgrounds, setShowBackgrounds] = useState(true);
const [diffIndicators, setDiffIndicators] = useState<DiffIndicators>('bars');
const [lineNumbers, setLineNumbers] = useState(true);
const [forceUnifiedDiffStyle, setForceUnifiedDiffStyle] = useState(false);
const {
displayPreferences,
displayPreferencesHydrated,
setCodeFont,
setDiffIndicators,
setDiffStyle,
setLineNumbers,
setOverflow,
setShowBackgrounds,
updateDisplayPreferences,
} = useDiffsHubDisplayPreferences();
const {
collapseMode,
codeFont,
diffIndicators,
lineNumbers,
overflow,
showBackgrounds,
} = displayPreferences;
const diffStyle = forceUnifiedDiffStyle
? 'unified'
: displayPreferences.diffStyle;
const codeFontFamily = getDiffsHubCodeFontFamily(codeFont);
// All theming state — color mode and the light/dark theme-name picks — lives
// in the single @pierre/theming controller (the same instance the app-wide
// ThemeProvider is bound to). Reading it here means picking Auto/Light/Dark
Expand Down Expand Up @@ -137,6 +156,7 @@ function ReviewUIInner({ domain, initialUrl, path }: ReviewUIProps) {
} = usePatchLoader({
collapseMode,
domain,
enabled: displayPreferencesHydrated,
onLoadStart: handlePatchLoadStart,
path,
viewerRef,
Expand All @@ -145,7 +165,7 @@ function ReviewUIInner({ domain, initialUrl, path }: ReviewUIProps) {
useEffect(() => {
const mediaQuery = window.matchMedia('(max-width: 767px)');
const updateMobileState = (matches: boolean) => {
setDiffStyle(matches ? 'unified' : 'split');
setForceUnifiedDiffStyle(matches);
if (!matches) setFileTreeOverlayOpen(false);
};
const handleChange = (event: MediaQueryListEvent) => {
Expand Down Expand Up @@ -177,9 +197,12 @@ function ReviewUIInner({ domain, initialUrl, path }: ReviewUIProps) {
}, []);
const handleToggleCollapseMode = useCallback(() => {
const next = collapseMode === 'expanded' ? 'collapsed' : 'expanded';
setCollapseMode(next);
updateDisplayPreferences((previous) => ({
...previous,
collapseMode: next,
}));
applyCollapseModeToLoaded(next);
}, [applyCollapseModeToLoaded, collapseMode]);
}, [applyCollapseModeToLoaded, collapseMode, updateDisplayPreferences]);
const handleCommentSaved = useCallback(
(comment: DiffsHubSavedCommentEvent) => {
setCommentSections((prev) =>
Expand Down Expand Up @@ -227,6 +250,7 @@ function ReviewUIInner({ domain, initialUrl, path }: ReviewUIProps) {
// first batch of files against the wrong palette.
const viewerAvailable =
isWorkerPoolReadyOrDisable &&
displayPreferencesHydrated &&
themesHydrated &&
(loadState === 'ready' ||
(loadState === 'streaming' && initialItems.length > 0));
Expand All @@ -235,6 +259,7 @@ function ReviewUIInner({ domain, initialUrl, path }: ReviewUIProps) {
<ReviewGrid>
<DiffsHubHeader
className="[grid-area:header]"
codeFont={codeFont}
collapseMode={collapseMode}
colorMode={colorMode}
darkThemeName={darkThemeName}
Expand All @@ -248,6 +273,7 @@ function ReviewUIInner({ domain, initialUrl, path }: ReviewUIProps) {
fileTreeAvailable={treeSource != null}
onToggleCollapseMode={handleToggleCollapseMode}
onToggleFileTreeOverlay={handleToggleFileTreeOverlay}
setCodeFont={setCodeFont}
setColorMode={setColorMode}
setDarkThemeName={setDarkThemeName}
setDiffIndicators={setDiffIndicators}
Expand Down Expand Up @@ -276,6 +302,7 @@ function ReviewUIInner({ domain, initialUrl, path }: ReviewUIProps) {
<DiffsHubViewer
key={viewerKey}
className="[grid-area:viewer]"
codeFontFamily={codeFontFamily}
diffStyle={diffStyle}
overflow={overflow}
showBackgrounds={showBackgrounds}
Expand Down
32 changes: 10 additions & 22 deletions apps/diffshub/components/themeController.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { createThemeController, type ThemePersistence } from '@pierre/theming';

import { docsThemeCatalog } from './themeCatalog';
import {
readBrowserStorageKey,
writeBrowserStorageKey,
} from '@/lib/browserStorage';

export { docsThemeCatalog } from './themeCatalog';

Expand All @@ -23,30 +27,14 @@ const MODE_KEY = 'theme';
const LIGHT_THEME_KEY = 'diffshub-light-theme';
const DARK_THEME_KEY = 'diffshub-dark-theme';

function readKey(key: string): string | null {
try {
return globalThis.localStorage?.getItem(key) ?? null;
} catch {
return null;
}
}

function writeKey(key: string, value: string): void {
try {
globalThis.localStorage?.setItem(key, value);
} catch {
// Storage may be unavailable (private mode / denied) — non-fatal.
}
}

// Maps the controller's selection onto the app's three storage keys: mode as a
// plain `light`/`dark`/`system` string under `theme` (what the bootstrap script
// reads), and the theme names under the diffshub-prefixed keys.
const docsPersistence: ThemePersistence = {
load() {
const mode = readKey(MODE_KEY);
const light = readKey(LIGHT_THEME_KEY);
const dark = readKey(DARK_THEME_KEY);
const mode = readBrowserStorageKey(MODE_KEY);
const light = readBrowserStorageKey(LIGHT_THEME_KEY);
const dark = readBrowserStorageKey(DARK_THEME_KEY);
if (mode == null && light == null && dark == null) return null;
const validMode =
mode === 'light' || mode === 'dark' || mode === 'system'
Expand All @@ -59,9 +47,9 @@ const docsPersistence: ThemePersistence = {
};
},
save(selection) {
writeKey(MODE_KEY, selection.mode);
writeKey(LIGHT_THEME_KEY, selection.lightThemeName);
writeKey(DARK_THEME_KEY, selection.darkThemeName);
writeBrowserStorageKey(MODE_KEY, selection.mode);
writeBrowserStorageKey(LIGHT_THEME_KEY, selection.lightThemeName);
writeBrowserStorageKey(DARK_THEME_KEY, selection.darkThemeName);
},
};

Expand Down
Loading