Skip to content
Merged
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
21 changes: 15 additions & 6 deletions packages/review-editor/components/DiffHunkPreview.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import React, { useMemo, useState, useEffect } from 'react';
import { FileDiff } from '@pierre/diffs/react';
import { getSingularPatch } from '@pierre/diffs';
import type { DiffLineBgIntensity } from '@plannotator/shared/config';
import { useTheme } from '@plannotator/ui/components/ThemeProvider';
import { useConfigValue } from '@plannotator/ui/config';
import { useReviewState } from '../dock/ReviewStateContext';
import { resolveSyntaxTheme } from '../hooks/usePierreTheme';
import { resolveSyntaxTheme, buildLineBgOverrides } from '../hooks/usePierreTheme';

interface DiffHunkPreviewProps {
/** Raw diff hunk string (unified diff format). */
Expand All @@ -17,7 +19,12 @@ interface DiffHunkPreviewProps {
* Build the unsafeCSS string for @pierre/diffs by reading computed CSS variables.
* Called synchronously so the first render is already themed (no flash on tooltip open).
*/
function buildPierreCSS(mode: 'dark' | 'light', fontFamily: string, fontSize: string): string {
function buildPierreCSS(
mode: 'light' | 'dark',
fontFamily: string,
fontSize: string,
lineBgIntensity: DiffLineBgIntensity,
): string {
try {
const styles = getComputedStyle(document.documentElement);
const bg = styles.getPropertyValue('--background').trim();
Expand All @@ -44,6 +51,7 @@ function buildPierreCSS(mode: 'dark' | 'light', fontFamily: string, fontSize: st
[data-file-info] { display: none !important; }
[data-diffs-header] { display: none !important; }
${fontCSS}
${buildLineBgOverrides(lineBgIntensity, mode)}
`;
} catch {
return '';
Expand All @@ -62,6 +70,7 @@ export const DiffHunkPreview: React.FC<DiffHunkPreviewProps> = ({
}) => {
const { resolvedMode, colorTheme } = useTheme();
const state = useReviewState();
const lineBgIntensity = useConfigValue('diffLineBgIntensity');
const [expanded, setExpanded] = useState(false);

const fileDiff = useMemo(() => {
Expand All @@ -86,19 +95,19 @@ export const DiffHunkPreview: React.FC<DiffHunkPreviewProps> = ({
// The lazy initializer reads computed CSS variables from the document root.
const [pierreTheme, setPierreTheme] = useState<{ type: 'dark' | 'light'; css: string }>(() => ({
type: resolvedMode ?? 'dark',
css: buildPierreCSS(resolvedMode ?? 'dark', state.fontFamily, state.fontSize),
css: buildPierreCSS(resolvedMode ?? 'dark', state.fontFamily, state.fontSize, lineBgIntensity),
}));

// Re-compute on theme / font changes
// Re-compute on theme / font / intensity changes
useEffect(() => {
const rafId = requestAnimationFrame(() => {
setPierreTheme({
type: resolvedMode ?? 'dark',
css: buildPierreCSS(resolvedMode ?? 'dark', state.fontFamily, state.fontSize),
css: buildPierreCSS(resolvedMode ?? 'dark', state.fontFamily, state.fontSize, lineBgIntensity),
});
});
return () => cancelAnimationFrame(rafId);
}, [resolvedMode, colorTheme, state.fontFamily, state.fontSize]);
}, [resolvedMode, colorTheme, state.fontFamily, state.fontSize, lineBgIntensity]);

const syntaxTheme = resolveSyntaxTheme(colorTheme, resolvedMode ?? 'dark');

Expand Down
7 changes: 7 additions & 0 deletions packages/review-editor/components/DiffOptionsPopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
OVERFLOW_OPTIONS,
INDICATOR_OPTIONS,
LINE_DIFF_OPTIONS,
LINE_BG_INTENSITY_OPTIONS,
} from '@plannotator/ui/components/Settings';

function CompactSegmented<T extends string>({ options, value, onChange }: {
Expand Down Expand Up @@ -95,6 +96,7 @@ export const DiffOptionsPopover: React.FC = () => {
const diffShowBackground = useConfigValue('diffShowBackground');
const diffHideWhitespace = useConfigValue('diffHideWhitespace');
const diffTabSize = useConfigValue('diffTabSize');
const diffLineBgIntensity = useConfigValue('diffLineBgIntensity');

return (
<Popover.Root>
Expand Down Expand Up @@ -144,6 +146,11 @@ export const DiffOptionsPopover: React.FC = () => {
<div>
<CompactToggle checked={diffShowLineNumbers} onChange={(v) => configStore.set('diffShowLineNumbers', v)} label="Line numbers" />
<CompactToggle checked={diffShowBackground} onChange={(v) => configStore.set('diffShowBackground', v)} label="Diff background" />
{diffShowBackground && (
<div className="pl-3 pr-0.5 pb-1 -mt-0.5">
<CompactSegmented options={LINE_BG_INTENSITY_OPTIONS} value={diffLineBgIntensity} onChange={(v) => configStore.set('diffLineBgIntensity', v)} />
</div>
)}
<CompactToggle checked={diffHideWhitespace} onChange={(v) => configStore.set('diffHideWhitespace', v)} label="Hide whitespace" />
<CompactStepper
label="Tab size"
Expand Down
104 changes: 103 additions & 1 deletion packages/review-editor/hooks/usePierreTheme.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useState, useEffect } from 'react';
import type { DiffLineBgIntensity } from '@plannotator/shared/config';
import { useTheme } from '@plannotator/ui/components/ThemeProvider';
import { useConfigValue } from '@plannotator/ui/config';

export const SHIKI_THEME_MAP: Record<string, { dark: string | null; light: string | null }> = {
'andromeeda': { dark: 'andromeeda', light: null },
Expand Down Expand Up @@ -51,11 +53,108 @@ export interface PierreTheme {
syntaxTheme?: { dark: string; light: string };
}

/**
* Bg-share percentages plugged into Pierre's `--mix-light` / `--mix-dark` —
* the share of decoration-bg in `color-mix(decoration-bg X%, mix-target)`
* inside Pierre's `light-dark()` switch (`Light` applies in light themes,
* `Dark` in dark themes). Lower number = more line colour. We mirror Pierre's
* own pattern of slightly lower values for dark themes (its defaults are
* 88 / 80) since darker themes need a larger colour share to read at the
* same perceptual intensity.
*
* Driving the line bg through these vars (instead of overriding the final
* `background-color`) keeps Pierre's `--diffs-line-bg` pipeline intact, so
* selected / hovered / decorated states keep their state-specific visuals.
*/
interface IntensityConfig {
restMixLight: number;
restMixDark: number;
hoverMixLight: number;
hoverMixDark: number;
}

const INTENSITY_CONFIG: Record<Exclude<DiffLineBgIntensity, 'subtle'>, IntensityConfig> = {
normal: { restMixLight: 55, restMixDark: 45, hoverMixLight: 45, hoverMixDark: 35 },
strong: { restMixLight: 35, restMixDark: 25, hoverMixLight: 25, hoverMixDark: 15 },
};

/**
* The word-level chip is derived from the *actual computed line bg* (not from
* the theme's addition/deletion base colour) and nudged by this OKLCH-`l`
* delta — darker on light themes, lighter on dark themes. Pulling it off the
* line bg keeps the chip-vs-line relationship constant across intensities:
* Normal and Strong each produce a chip that's "one step deeper than this
* specific line", instead of one fixed chip that fights more or less against
* different lines.
*/
const EMPHASIS_LIGHTNESS_SHIFT = 0.07;

/**
* @pierre/diffs hardcodes the diff-line bg as a ~12-20% mix of the line colour
* over the gutter (`--mix-light: 88%` / `--mix-dark: 80%`). To get a bolder
* look we lower those percentages on changed lines, so the library's existing
* `--diffs-line-bg` pipeline naturally produces stronger output. The hue comes
* from the resolved theme tokens (`--diffs-addition-base` /
* `--diffs-deletion-base`) — themes that customize diff colours keep them.
*
* `subtle` keeps Pierre's default line bg (its faint mix + alpha-overlay
* emphasis is exactly what Pierre's design intends), but still emits the
* "hide emphasis when diff bg is off" rule so that toggle behaves consistently
* at every intensity.
*/
export function buildLineBgOverrides(intensity: DiffLineBgIntensity, mode: 'light' | 'dark'): string {
// The library's word-emphasis rule (`[data-line-type=…] [data-diff-span] {
// background-color: var(--diffs-bg-addition-emphasis); }`) is NOT gated on
// `[data-background]`, so disabling diff backgrounds still leaves chips
// showing on plain lines. We hide them explicitly. Applies regardless of
// intensity so the "Diff background" toggle behaves consistently.
const hideEmphasisWithoutBg = `
pre:not([data-background]) [data-line-type='change-addition'] [data-diff-span],
pre:not([data-background]) [data-line-type='change-deletion'] [data-diff-span] {
background-color: transparent !important;
}
`;
if (intensity === 'subtle') return hideEmphasisWithoutBg;
const cfg = INTENSITY_CONFIG[intensity];
const lShift = mode === 'dark'
? `+ ${EMPHASIS_LIGHTNESS_SHIFT}`
: `- ${EMPHASIS_LIGHTNESS_SHIFT}`;
// Targeting `[data-line]` and `[data-no-newline]` only — the actual code
// lines. Skipping `[data-gutter-buffer]` / `[data-column-number]` keeps the
// line-number gutter at the page bg (matching the existing
// `[data-column-number] { background-color: bg }` integration). Gating on
// `[data-background]` mirrors the library's own `:where([data-background])`
// scoping, so the "Diff background" toggle still turns line bgs off.
//
// Specificity is (0,0,4); wins against the library's (0,0,1) baseline and
// (0,0,3) hover rule. The `:not([data-hovered])` variant yields to the
// explicit `[data-hovered]` variant on hover.
const changedLine =
"[data-background] :is([data-line-type='change-addition'], [data-line-type='change-deletion'])" +
":is([data-line], [data-no-newline])";
return `
${changedLine}:not([data-hovered]) {
--mix-light: ${cfg.restMixLight}%;
--mix-dark: ${cfg.restMixDark}%;
}
${changedLine}[data-hovered] {
--mix-light: ${cfg.hoverMixLight}%;
--mix-dark: ${cfg.hoverMixDark}%;
}
${changedLine} {
--diffs-bg-addition-emphasis: oklch(from var(--diffs-computed-diff-line-bg) calc(l ${lShift}) c h);
--diffs-bg-deletion-emphasis: oklch(from var(--diffs-computed-diff-line-bg) calc(l ${lShift}) c h);
}
${hideEmphasisWithoutBg}
`;
}

export function usePierreTheme(options?: { fontFamily?: string; fontSize?: string; showFileHeader?: boolean }): PierreTheme {
const { colorTheme, resolvedMode } = useTheme();
const fontFamily = options?.fontFamily;
const fontSize = options?.fontSize;
const showFileHeader = options?.showFileHeader ?? false;
const lineBgIntensity = useConfigValue('diffLineBgIntensity');

const [pierreTheme, setPierreTheme] = useState<PierreTheme>(() => {
const styles = getComputedStyle(document.documentElement);
Expand All @@ -71,6 +170,7 @@ export function usePierreTheme(options?: { fontFamily?: string; fontSize?: strin
:host { --diffs-bg-separator-override: color-mix(in srgb, ${fg} 8%, ${bg}); }
[data-separator='line-info'], [data-separator='line-info-basic'] { height: 24px !important; }
[data-separator='line-info'] { margin-block: 4px !important; }
${buildLineBgOverrides(lineBgIntensity, resolvedMode ?? 'dark')}
`};
});

Expand Down Expand Up @@ -170,10 +270,12 @@ export function usePierreTheme(options?: { fontFamily?: string; fontSize?: strin
}

${fontCSS}

${buildLineBgOverrides(lineBgIntensity, resolvedMode)}
`,
});
});
}, [resolvedMode, colorTheme, fontFamily, fontSize, showFileHeader]);
}, [resolvedMode, colorTheme, fontFamily, fontSize, showFileHeader, lineBgIntensity]);

return pierreTheme;
}
17 changes: 0 additions & 17 deletions packages/review-editor/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -112,23 +112,6 @@ diffs-container {
--diffs-light: var(--foreground);
}

/* Diff line backgrounds - synchronized with plannotator colors */
.diff-line-addition {
background: oklch(from var(--success) l c h / 0.12);
}

.diff-line-deletion {
background: oklch(from var(--destructive) l c h / 0.12);
}

.light .diff-line-addition {
background: oklch(from var(--success) l c h / 0.15);
}

.light .diff-line-deletion {
background: oklch(from var(--destructive) l c h / 0.15);
}

/* Review comment thread styling */
.review-comment {
position: relative;
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
import { execSync } from "child_process";

export type DefaultDiffType = 'uncommitted' | 'unstaged' | 'staged' | 'merge-base' | 'all';
export type DiffLineBgIntensity = 'subtle' | 'normal' | 'strong';

export interface DiffOptions {
diffStyle?: 'split' | 'unified';
Expand All @@ -24,6 +25,7 @@ export interface DiffOptions {
tabSize?: number;
hideWhitespace?: boolean;
defaultDiffType?: DefaultDiffType;
lineBgIntensity?: DiffLineBgIntensity;
}

/** Single conventional comment label entry stored in config.json */
Expand Down
18 changes: 18 additions & 0 deletions packages/ui/components/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState, useEffect, useMemo } from 'react';
import { createPortal } from 'react-dom';
import type { Origin } from '@plannotator/shared/agents';
import type { DiffLineBgIntensity } from '@plannotator/shared/config';
import { configStore, useConfigValue } from '../config';
import { loadDiffFont } from '../utils/diffFonts';
import { TaterSpritePullup } from './TaterSpritePullup';
Expand Down Expand Up @@ -122,6 +123,11 @@ export const LINE_DIFF_OPTIONS = [
{ value: 'char' as const, label: 'Char' },
{ value: 'none' as const, label: 'None' },
];
export const LINE_BG_INTENSITY_OPTIONS: { value: DiffLineBgIntensity; label: string }[] = [
{ value: 'subtle', label: 'Subtle' },
{ value: 'normal', label: 'Normal' },
{ value: 'strong', label: 'Strong' },
];
const DEFAULT_DIFF_TYPE_OPTIONS = [
{ value: 'uncommitted' as const, label: 'All Changes', description: "Everything you've changed since your last commit" },
{ value: 'unstaged' as const, label: 'Unstaged', description: "Only changes you haven't staged yet" },
Expand Down Expand Up @@ -229,6 +235,7 @@ const ReviewDisplayTab: React.FC = () => {
const diffLineDiffType = useConfigValue('diffLineDiffType');
const diffShowLineNumbers = useConfigValue('diffShowLineNumbers');
const diffShowBackground = useConfigValue('diffShowBackground');
const diffLineBgIntensity = useConfigValue('diffLineBgIntensity');
const diffHideWhitespace = useConfigValue('diffHideWhitespace');
const diffFontFamily = useConfigValue('diffFontFamily');
const diffFontSize = useConfigValue('diffFontSize');
Expand Down Expand Up @@ -363,6 +370,17 @@ const ReviewDisplayTab: React.FC = () => {
description="Colored backgrounds on added/deleted lines"
/>

{/* Line Background Intensity */}
{diffShowBackground && (
<div className="space-y-2 pl-4">
<div>
<div className="text-sm font-medium">Line Background Intensity</div>
<div className="text-xs text-muted-foreground">How prominent the colored line backgrounds appear</div>
</div>
<SegmentedControl options={LINE_BG_INTENSITY_OPTIONS} value={diffLineBgIntensity} onChange={(v) => configStore.set('diffLineBgIntensity', v)} />
</div>
)}

<div className="border-t border-border" />

{/* Hide Whitespace */}
Expand Down
21 changes: 21 additions & 0 deletions packages/ui/config/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@
* Add new settings here. Cookie-only settings omit serverKey.
*/

import type { DiffLineBgIntensity } from '@plannotator/shared/config';
import { storage } from '../utils/storage';
import { generateIdentity } from '../utils/generateIdentity';

const DIFF_LINE_BG_INTENSITY_VALUES = ['subtle', 'normal', 'strong'] as const;
function isDiffLineBgIntensity(v: unknown): v is DiffLineBgIntensity {
return typeof v === 'string' && (DIFF_LINE_BG_INTENSITY_VALUES as readonly string[]).includes(v);
}

export interface SettingDef<T> {
defaultValue: T | (() => T);
fromCookie: () => T | undefined;
Expand Down Expand Up @@ -195,6 +201,21 @@ export const SETTINGS = {
},
toServer: (v: number) => ({ diffOptions: { tabSize: v } }),
},
diffLineBgIntensity: {
defaultValue: 'subtle' as DiffLineBgIntensity,
fromCookie: () => {
const v = storage.getItem('plannotator-diff-line-bg-intensity');
return isDiffLineBgIntensity(v) ? v : undefined;
},
toCookie: (v: DiffLineBgIntensity) =>
storage.setItem('plannotator-diff-line-bg-intensity', v),
serverKey: 'diffOptions',
fromServer: (sc: Record<string, unknown>) => {
const v = (sc.diffOptions as Record<string, unknown> | undefined)?.lineBgIntensity;
return isDiffLineBgIntensity(v) ? v : undefined;
},
toServer: (v: DiffLineBgIntensity) => ({ diffOptions: { lineBgIntensity: v } }),
},
conventionalComments: {
defaultValue: false as boolean,
fromCookie: () => {
Expand Down
Loading