mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-19 01:51:15 -05:00
refac
This commit is contained in:
1 parent
c143625cc4
commit
de642f1dba
1 file changed
+301
-164
@@ -3,7 +3,7 @@
|
||||
import { getContext } from 'svelte';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
import { basicSetup, EditorView } from 'codemirror';
|
||||
import { Compartment, type Text } from '@codemirror/state';
|
||||
import { Compartment, EditorState, type Text } from '@codemirror/state';
|
||||
import { keymap, type ViewUpdate } from '@codemirror/view';
|
||||
import { indentWithTab, isolateHistory } from '@codemirror/commands';
|
||||
import { LanguageDescription, indentUnit } from '@codemirror/language';
|
||||
@@ -30,6 +30,12 @@
|
||||
type MergeDirection
|
||||
} from './diffEditor';
|
||||
|
||||
type FileLabel = { name: string; path: string };
|
||||
type ComparisonTexts = { initialContent: string; proposedContent: string };
|
||||
export let mode: 'edit' | 'review' = 'edit';
|
||||
export let leftFile: FileLabel | undefined = undefined;
|
||||
export let rightFile: FileLabel | undefined = undefined;
|
||||
export let onRefresh: (() => Promise<ComparisonTexts>) | undefined = undefined;
|
||||
export let show = false;
|
||||
export let initialContent = '';
|
||||
export let proposedContent: string | undefined = undefined;
|
||||
@@ -47,6 +53,11 @@
|
||||
proposal: string;
|
||||
path: string | null;
|
||||
title: string;
|
||||
mode: 'edit' | 'review';
|
||||
hasProposal: boolean;
|
||||
leftFile?: FileLabel;
|
||||
rightFile?: FileLabel;
|
||||
onRefresh?: () => Promise<ComparisonTexts>;
|
||||
} | null = null;
|
||||
let working = '';
|
||||
let proposal = '';
|
||||
@@ -63,8 +74,32 @@
|
||||
let error = '';
|
||||
let confirmDiscard = false;
|
||||
|
||||
let swapped = false;
|
||||
let refreshing = false;
|
||||
let editorRevision = 0;
|
||||
$: displayedFiles = swapped
|
||||
? [session?.rightFile, session?.leftFile]
|
||||
: [session?.leftFile, session?.rightFile];
|
||||
const paneFile = (side: string) =>
|
||||
(side === 'a') !== swapped ? session?.leftFile : session?.rightFile;
|
||||
const paneLabel = (side: string) =>
|
||||
paneFile(side)?.name ?? (side === 'a' ? $i18n.t('Original') : $i18n.t('Proposed revision'));
|
||||
const parentPath = (path: string) => path.replace(/[\\/][^\\/]*$/, '') || '/';
|
||||
|
||||
const begin = () => {
|
||||
session = { original: initialContent, proposal: proposedContent ?? '', path: filePath, title };
|
||||
session = {
|
||||
original: initialContent,
|
||||
proposal: proposedContent ?? '',
|
||||
path: filePath,
|
||||
title,
|
||||
mode,
|
||||
hasProposal: proposedContent !== undefined,
|
||||
leftFile: leftFile && { ...leftFile },
|
||||
rightFile: rightFile && { ...rightFile },
|
||||
onRefresh
|
||||
};
|
||||
swapped = refreshing = false;
|
||||
editorRevision++;
|
||||
working = session.original;
|
||||
proposal = session.proposal;
|
||||
comparing = proposedContent !== undefined;
|
||||
@@ -73,7 +108,10 @@
|
||||
lineChanges = { removed: 0, added: 0 };
|
||||
copied = null;
|
||||
loading = approximate = false;
|
||||
error = '';
|
||||
error =
|
||||
mode === 'review' && proposedContent === undefined
|
||||
? $i18n.t('Both files are required for comparison.')
|
||||
: '';
|
||||
confirmDiscard = false;
|
||||
};
|
||||
$: if (show && !session) begin();
|
||||
@@ -87,11 +125,13 @@
|
||||
onClose();
|
||||
};
|
||||
const requestClose = () => {
|
||||
if (session?.mode === 'review') return close();
|
||||
if (session && (working !== session.original || proposal !== session.proposal))
|
||||
confirmDiscard = true;
|
||||
else close();
|
||||
};
|
||||
const apply = () => {
|
||||
if (session?.mode === 'review') return;
|
||||
try {
|
||||
onApply(proposal);
|
||||
close();
|
||||
@@ -99,6 +139,36 @@
|
||||
error = cause instanceof Error ? cause.message : String(cause);
|
||||
}
|
||||
};
|
||||
const replaceReview = (texts: ComparisonTexts) => {
|
||||
working = swapped ? texts.proposedContent : texts.initialContent;
|
||||
proposal = swapped ? texts.initialContent : texts.proposedContent;
|
||||
activeChunk = 0;
|
||||
copied = null;
|
||||
editorRevision++;
|
||||
};
|
||||
const swap = () => {
|
||||
if (session?.mode !== 'review') return;
|
||||
swapped = !swapped;
|
||||
replaceReview({ initialContent: session.original, proposedContent: session.proposal });
|
||||
};
|
||||
const reload = async () => {
|
||||
const current = session;
|
||||
if (current?.mode !== 'review' || !current.onRefresh || refreshing) return;
|
||||
refreshing = true;
|
||||
error = '';
|
||||
try {
|
||||
const texts = await current.onRefresh();
|
||||
if (session !== current || !show) return;
|
||||
current.original = texts.initialContent;
|
||||
current.proposal = texts.proposedContent;
|
||||
replaceReview(texts);
|
||||
} catch (cause) {
|
||||
if (session === current && show)
|
||||
error = cause instanceof Error ? cause.message : String(cause);
|
||||
} finally {
|
||||
if (session === current) refreshing = false;
|
||||
}
|
||||
};
|
||||
const copyPane = async (side: 'a' | 'b') => {
|
||||
const text = side === 'a' ? working : proposal;
|
||||
const currentSession = session;
|
||||
@@ -118,8 +188,10 @@
|
||||
const refresh = (update?: ViewUpdate, side: 'a' | 'b' = 'a') => {
|
||||
if (!merge) return;
|
||||
if (!update || update.docChanged) {
|
||||
working = merge.a.state.doc.toString();
|
||||
proposal = merge.b.state.doc.toString();
|
||||
if (session?.mode !== 'review') {
|
||||
working = merge.a.state.doc.toString();
|
||||
proposal = merge.b.state.doc.toString();
|
||||
}
|
||||
chunkCount = merge.chunks.length;
|
||||
lineNumberDigits = String(Math.max(merge.a.state.doc.lines, merge.b.state.doc.lines)).length;
|
||||
lineChanges = changedLineCounts(merge.a.state.doc, merge.b.state.doc, merge.chunks);
|
||||
@@ -169,7 +241,7 @@
|
||||
};
|
||||
|
||||
const copy = (direction: MergeDirection) => {
|
||||
if (!merge || !merge.chunks[activeChunk]) return;
|
||||
if (session?.mode === 'review' || !merge || !merge.chunks[activeChunk]) return;
|
||||
const destination = direction === 'to-working' ? merge.a : merge.b;
|
||||
destination.dispatch({
|
||||
changes: copyChunk(
|
||||
@@ -193,16 +265,20 @@
|
||||
let alignmentGeometry = '';
|
||||
const themes = [new Compartment(), new Compartment()];
|
||||
const language = [new Compartment(), new Compartment()];
|
||||
const path = session?.path;
|
||||
const paths = ['a', 'b'].map((side) => paneFile(side)?.path ?? session?.path);
|
||||
const review = session?.mode === 'review';
|
||||
loading = true;
|
||||
const extensions = (side: 'a' | 'b', index: number) => [
|
||||
basicSetup,
|
||||
EditorState.readOnly.of(review),
|
||||
EditorView.editable.of(!review),
|
||||
lineAlignment,
|
||||
keymap.of([indentWithTab, { key: 'Mod-s', run: () => true }]),
|
||||
indentUnit.of(' '),
|
||||
EditorView.lineWrapping,
|
||||
EditorView.contentAttributes.of({
|
||||
'aria-label': side === 'a' ? $i18n.t('Original') : $i18n.t('Proposed revision')
|
||||
'aria-label': paneLabel(side),
|
||||
...(review ? { tabindex: '0', 'aria-readonly': 'true' } : {})
|
||||
}),
|
||||
themes[index].of(document.documentElement.classList.contains('dark') ? oneDark : []),
|
||||
language[index].of([]),
|
||||
@@ -289,13 +365,17 @@
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
});
|
||||
const match = path && LanguageDescription.matchFilename(languages, path);
|
||||
if (match) {
|
||||
const support = await match.load();
|
||||
if (disposed) return;
|
||||
view.a.dispatch({ effects: language[0].reconfigure(support) });
|
||||
view.b.dispatch({ effects: language[1].reconfigure(support) });
|
||||
}
|
||||
await Promise.all(
|
||||
paths.map(async (path, index) => {
|
||||
const match = path && LanguageDescription.matchFilename(languages, path);
|
||||
if (!match) return;
|
||||
const support = await match.load();
|
||||
if (!disposed)
|
||||
(index === 0 ? view!.a : view!.b).dispatch({
|
||||
effects: language[index].reconfigure(support)
|
||||
});
|
||||
})
|
||||
);
|
||||
} catch (cause) {
|
||||
if (!disposed) {
|
||||
error = cause instanceof Error ? cause.message : String(cause);
|
||||
@@ -331,7 +411,7 @@
|
||||
<h2 class="font-medium">{session.title || $i18n.t('Compare revision')}</h2>
|
||||
{#if session.path}<p class="mt-1 truncate font-mono text-gray-500">{session.path}</p>{/if}
|
||||
</div>
|
||||
{#if !comparing}
|
||||
{#if !comparing && session.mode === 'edit'}
|
||||
<button type="button" class="primary" on:click={() => (comparing = true)}
|
||||
>{$i18n.t('Compare')}</button
|
||||
>
|
||||
@@ -374,22 +454,41 @@
|
||||
disabled={!chunkCount || activeChunk === chunkCount - 1}
|
||||
on:click={() => reveal(activeChunk + 1)}><span aria-hidden="true">↓</span></button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="secondary"
|
||||
disabled={!chunkCount}
|
||||
aria-label={$i18n.t('Copy change to original')}
|
||||
title={$i18n.t('Copy change to original')}
|
||||
on:click={() => copy('to-working')}>← {$i18n.t('Original')}</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="secondary"
|
||||
disabled={!chunkCount}
|
||||
aria-label={$i18n.t('Copy change to proposal')}
|
||||
title={$i18n.t('Copy change to proposal')}
|
||||
on:click={() => copy('to-proposal')}>{$i18n.t('Proposed revision')} →</button
|
||||
>
|
||||
{#if session.mode === 'edit'}
|
||||
<button
|
||||
type="button"
|
||||
class="secondary"
|
||||
disabled={!chunkCount}
|
||||
aria-label={$i18n.t('Copy change to original')}
|
||||
title={$i18n.t('Copy change to original')}
|
||||
on:click={() => copy('to-working')}>← {$i18n.t('Original')}</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="secondary"
|
||||
disabled={!chunkCount}
|
||||
aria-label={$i18n.t('Copy change to proposal')}
|
||||
title={$i18n.t('Copy change to proposal')}
|
||||
on:click={() => copy('to-proposal')}>{$i18n.t('Proposed revision')} →</button
|
||||
>
|
||||
{:else}
|
||||
<button type="button" class="secondary" disabled={loading} on:click={swap}
|
||||
>{$i18n.t('Swap sides')}</button
|
||||
>
|
||||
{#if session.onRefresh}
|
||||
<button
|
||||
type="button"
|
||||
class="secondary"
|
||||
disabled={refreshing || loading}
|
||||
on:click={reload}
|
||||
>{refreshing
|
||||
? $i18n.t('Refreshing...')
|
||||
: error
|
||||
? $i18n.t('Retry')
|
||||
: $i18n.t('Refresh')}</button
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
@@ -400,140 +499,161 @@
|
||||
>
|
||||
</header>
|
||||
{#if error}<p class="px-4 py-2 text-red-600" role="alert">{error}</p>{/if}
|
||||
<div class="relative flex min-h-0 flex-1">
|
||||
<div class="min-h-0 min-w-0 flex-1 overflow-x-auto px-4">
|
||||
<div class="flex h-full min-w-[640px] flex-col">
|
||||
<div class="grid shrink-0 grid-cols-2 gap-4 py-2">
|
||||
{#each ['a', 'b'] as side}
|
||||
{@const lines = (side === 'a' ? working : proposal).split('\n').length}
|
||||
<div
|
||||
class="pane-header flex min-w-0 items-center gap-2 rounded-lg bg-gray-50/70 px-3 py-2 text-gray-500 dark:bg-white/[0.03]"
|
||||
>
|
||||
<span class="min-w-0 truncate font-medium"
|
||||
>{side === 'a' ? $i18n.t('Original') : $i18n.t('Proposed revision')}</span
|
||||
{#if session.mode === 'edit' || session.hasProposal}
|
||||
<div class="relative flex min-h-0 flex-1">
|
||||
<div class="min-h-0 min-w-0 flex-1 overflow-x-auto px-4">
|
||||
<div class="flex h-full min-w-[640px] flex-col">
|
||||
<div class="grid shrink-0 grid-cols-2 gap-4 py-2">
|
||||
{#each ['a', 'b'] as side, index}
|
||||
{@const lines = (side === 'a' ? working : proposal).split(/\r\n|\r|\n/).length}
|
||||
{@const file = displayedFiles[index]}
|
||||
<div
|
||||
class="pane-header flex min-w-0 items-center gap-2 rounded-lg bg-gray-50/70 px-3 py-2 text-gray-500 dark:bg-white/[0.03]"
|
||||
>
|
||||
{#if comparing && !loading}
|
||||
<span
|
||||
class="whitespace-nowrap"
|
||||
class:text-red-600={side === 'a'}
|
||||
class:dark:text-red-400={side === 'a'}
|
||||
class:text-teal-700={side === 'b'}
|
||||
class:dark:text-teal-400={side === 'b'}
|
||||
title={$i18n.t('Lines containing changed text')}
|
||||
<div class="min-w-0" title={file?.path}>
|
||||
<span class="block truncate font-medium"
|
||||
>{file?.name ??
|
||||
(side === 'a' ? $i18n.t('Original') : $i18n.t('Proposed revision'))}</span
|
||||
>
|
||||
{#if file}<span class="block truncate text-[10px]" aria-label={file.path}
|
||||
>{parentPath(file.path)}</span
|
||||
>{/if}
|
||||
</div>
|
||||
{#if comparing && !loading}
|
||||
<span
|
||||
class="whitespace-nowrap"
|
||||
class:text-red-600={side === 'a'}
|
||||
class:dark:text-red-400={side === 'a'}
|
||||
class:text-teal-700={side === 'b'}
|
||||
class:dark:text-teal-400={side === 'b'}
|
||||
title={$i18n.t('Lines containing changed text')}
|
||||
>
|
||||
<span aria-hidden="true">{side === 'a' ? '−' : '+'}</span>
|
||||
{side === 'a'
|
||||
? $i18n.t(
|
||||
lineChanges.removed === 1
|
||||
? '{{count}} removal'
|
||||
: '{{count}} removals',
|
||||
{ count: lineChanges.removed }
|
||||
)
|
||||
: $i18n.t(
|
||||
lineChanges.added === 1
|
||||
? '{{count}} addition'
|
||||
: '{{count}} additions',
|
||||
{ count: lineChanges.added }
|
||||
)}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="ml-auto whitespace-nowrap text-[11px]"
|
||||
>{$i18n.t(lines === 1 ? '{{count}} line' : '{{count}} lines', {
|
||||
count: lines
|
||||
})}</span
|
||||
>
|
||||
<span aria-hidden="true">{side === 'a' ? '−' : '+'}</span>
|
||||
{side === 'a'
|
||||
? $i18n.t(
|
||||
lineChanges.removed === 1 ? '{{count}} removal' : '{{count}} removals',
|
||||
{ count: lineChanges.removed }
|
||||
)
|
||||
: $i18n.t(
|
||||
lineChanges.added === 1 ? '{{count}} addition' : '{{count}} additions',
|
||||
{ count: lineChanges.added }
|
||||
)}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="ml-auto whitespace-nowrap text-[11px]"
|
||||
>{$i18n.t(lines === 1 ? '{{count}} line' : '{{count}} lines', {
|
||||
count: lines
|
||||
})}</span
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded px-1 hover:text-gray-900 dark:hover:text-white"
|
||||
aria-label={side === 'a'
|
||||
? $i18n.t('Copy original to clipboard')
|
||||
: $i18n.t('Copy proposed revision to clipboard')}
|
||||
on:click={() => copyPane(side === 'a' ? 'a' : 'b')}
|
||||
>
|
||||
{copied?.side === side && copied.text === (side === 'a' ? working : proposal)
|
||||
? $i18n.t('Copied')
|
||||
: $i18n.t('Copy')}
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if comparing}
|
||||
<div
|
||||
class="merge-container min-h-0 flex-1"
|
||||
style:--diff-line-digits={lineNumberDigits}
|
||||
use:mountMerge
|
||||
></div>
|
||||
{:else}
|
||||
<div class="grid min-h-0 flex-1 grid-cols-2 gap-4">
|
||||
<FileCodeEditor
|
||||
bind:value={working}
|
||||
filePath={session.path}
|
||||
ariaLabel={$i18n.t('Original')}
|
||||
/>
|
||||
<div class="min-h-0 min-w-0">
|
||||
<FileCodeEditor
|
||||
bind:value={proposal}
|
||||
filePath={session.path}
|
||||
ariaLabel={$i18n.t('Proposed revision')}
|
||||
placeholder={$i18n.t('Paste a proposed revision here...')}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 rounded px-1 hover:text-gray-900 dark:hover:text-white"
|
||||
aria-label={file
|
||||
? $i18n.t('Copy {{name}} to clipboard', { name: file.name })
|
||||
: side === 'a'
|
||||
? $i18n.t('Copy original to clipboard')
|
||||
: $i18n.t('Copy proposed revision to clipboard')}
|
||||
on:click={() => copyPane(side === 'a' ? 'a' : 'b')}
|
||||
>
|
||||
{copied?.side === side && copied.text === (side === 'a' ? working : proposal)
|
||||
? $i18n.t('Copied')
|
||||
: $i18n.t('Copy')}
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if comparing}
|
||||
{#key editorRevision}
|
||||
<div
|
||||
class="merge-container min-h-0 flex-1"
|
||||
style:--diff-line-digits={lineNumberDigits}
|
||||
use:mountMerge
|
||||
></div>
|
||||
{/key}
|
||||
{:else}
|
||||
<div class="grid min-h-0 flex-1 grid-cols-2 gap-4">
|
||||
<FileCodeEditor
|
||||
bind:value={working}
|
||||
filePath={session.path}
|
||||
ariaLabel={$i18n.t('Original')}
|
||||
/>
|
||||
<div class="min-h-0 min-w-0">
|
||||
<FileCodeEditor
|
||||
bind:value={proposal}
|
||||
filePath={session.path}
|
||||
ariaLabel={$i18n.t('Proposed revision')}
|
||||
placeholder={$i18n.t('Paste a proposed revision here...')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if comparing}
|
||||
<div
|
||||
class="diff-overview"
|
||||
role="slider"
|
||||
tabindex={chunkCount ? 0 : -1}
|
||||
aria-label={$i18n.t('Change overview')}
|
||||
aria-orientation="vertical"
|
||||
aria-valuemin={1}
|
||||
aria-valuemax={Math.max(1, chunkCount)}
|
||||
aria-valuenow={activeChunk + 1}
|
||||
aria-valuetext={chunkCount
|
||||
? $i18n.t('Change {{current}} of {{total}}', {
|
||||
current: activeChunk + 1,
|
||||
total: chunkCount
|
||||
})
|
||||
: $i18n.t('No differences')}
|
||||
aria-disabled={!chunkCount}
|
||||
on:click={navigateOverview}
|
||||
on:keydown={overviewKeydown}
|
||||
title={$i18n.t('Click to jump to a change. Use arrow keys to navigate.')}
|
||||
>
|
||||
<svg viewBox="0 0 20 1000" preserveAspectRatio="none" aria-hidden="true">
|
||||
<rect class="overview-track" x="2" y="0" width="6" height="1000" rx="3" />
|
||||
<rect class="overview-track" x="12" y="0" width="6" height="1000" rx="3" />
|
||||
{#each overview as range, index}
|
||||
<rect
|
||||
class="overview-removed"
|
||||
class:overview-active={index === activeChunk}
|
||||
x="2"
|
||||
y={Math.min(997, range.a.from * 1000)}
|
||||
width="6"
|
||||
height={Math.max(3, (range.a.to - range.a.from) * 1000)}
|
||||
/>
|
||||
<rect
|
||||
class="overview-added"
|
||||
class:overview-active={index === activeChunk}
|
||||
x="12"
|
||||
y={Math.min(997, range.b.from * 1000)}
|
||||
width="6"
|
||||
height={Math.max(3, (range.b.to - range.b.from) * 1000)}
|
||||
/>
|
||||
{/each}
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if comparing}
|
||||
<div
|
||||
class="diff-overview"
|
||||
role="slider"
|
||||
tabindex={chunkCount ? 0 : -1}
|
||||
aria-label={$i18n.t('Change overview')}
|
||||
aria-orientation="vertical"
|
||||
aria-valuemin={1}
|
||||
aria-valuemax={Math.max(1, chunkCount)}
|
||||
aria-valuenow={activeChunk + 1}
|
||||
aria-valuetext={chunkCount
|
||||
? $i18n.t('Change {{current}} of {{total}}', {
|
||||
current: activeChunk + 1,
|
||||
total: chunkCount
|
||||
})
|
||||
: $i18n.t('No differences')}
|
||||
aria-disabled={!chunkCount}
|
||||
on:click={navigateOverview}
|
||||
on:keydown={overviewKeydown}
|
||||
title={$i18n.t('Click to jump to a change. Use arrow keys to navigate.')}
|
||||
{/if}
|
||||
{#if session.mode === 'edit'}
|
||||
<footer class="flex shrink-0 flex-wrap items-center gap-2 px-5 py-3">
|
||||
<p class="mr-auto text-gray-500">
|
||||
{applyDescription ??
|
||||
$i18n.t('Apply uses the proposed revision on the right. Save your draft afterward.')}
|
||||
</p>
|
||||
<button type="button" class="secondary" on:click={requestClose}
|
||||
>{$i18n.t('Cancel')}</button
|
||||
>
|
||||
<svg viewBox="0 0 20 1000" preserveAspectRatio="none" aria-hidden="true">
|
||||
<rect class="overview-track" x="2" y="0" width="6" height="1000" rx="3" />
|
||||
<rect class="overview-track" x="12" y="0" width="6" height="1000" rx="3" />
|
||||
{#each overview as range, index}
|
||||
<rect
|
||||
class="overview-removed"
|
||||
class:overview-active={index === activeChunk}
|
||||
x="2"
|
||||
y={Math.min(997, range.a.from * 1000)}
|
||||
width="6"
|
||||
height={Math.max(3, (range.a.to - range.a.from) * 1000)}
|
||||
/>
|
||||
<rect
|
||||
class="overview-added"
|
||||
class:overview-active={index === activeChunk}
|
||||
x="12"
|
||||
y={Math.min(997, range.b.from * 1000)}
|
||||
width="6"
|
||||
height={Math.max(3, (range.b.to - range.b.from) * 1000)}
|
||||
/>
|
||||
{/each}
|
||||
</svg>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<footer class="flex shrink-0 flex-wrap items-center gap-2 px-5 py-3">
|
||||
<p class="mr-auto text-gray-500">
|
||||
{applyDescription ??
|
||||
$i18n.t('Apply uses the proposed revision on the right. Save your draft afterward.')}
|
||||
</p>
|
||||
<button type="button" class="secondary" on:click={requestClose}>{$i18n.t('Cancel')}</button>
|
||||
<button type="button" class="primary" disabled={loading} on:click={apply}
|
||||
>{applyLabel ?? $i18n.t('Apply to draft')}</button
|
||||
>
|
||||
</footer>
|
||||
<button type="button" class="primary" disabled={loading} on:click={apply}
|
||||
>{applyLabel ?? $i18n.t('Apply to draft')}</button
|
||||
>
|
||||
</footer>
|
||||
{/if}
|
||||
</div>
|
||||
</Modal>
|
||||
<ConfirmDialog
|
||||
@@ -673,16 +793,33 @@
|
||||
background: repeating-linear-gradient(135deg, transparent 0 3px, #94a3b814 3px 4px);
|
||||
}
|
||||
:global(.dark) .merge-container :global(.cm-merge-a .cm-changedLine) {
|
||||
background: #ef6b5520;
|
||||
background: #3d1915;
|
||||
color: #f4d0c7;
|
||||
}
|
||||
:global(.dark) .merge-container :global(.cm-merge-b .cm-changedLine) {
|
||||
background: #2dd4bf18;
|
||||
background: #0d302a;
|
||||
color: #a9e9dd;
|
||||
}
|
||||
:global(.dark) .merge-container :global(.cm-changedLine span) {
|
||||
color: inherit;
|
||||
}
|
||||
:global(.dark) .merge-container :global(.cm-merge-a .cm-changedText) {
|
||||
background: #ef6b5555;
|
||||
background: #77362c;
|
||||
}
|
||||
:global(.dark) .merge-container :global(.cm-merge-b .cm-changedText) {
|
||||
background: #2dd4bf45;
|
||||
background: #176456;
|
||||
}
|
||||
:global(.dark) .merge-container :global(.cm-merge-a .cm-changedLineGutter) {
|
||||
background: #9b5549;
|
||||
}
|
||||
:global(.dark) .merge-container :global(.cm-merge-b .cm-changedLineGutter) {
|
||||
background: #3e9c89;
|
||||
}
|
||||
:global(.dark) .merge-container :global(.cm-editor) {
|
||||
background: #101413;
|
||||
}
|
||||
:global(.dark) .merge-container :global(.cm-gutters) {
|
||||
background: #101413 !important;
|
||||
}
|
||||
.merge-container :global(.cm-mergeViewEditors) {
|
||||
gap: 1rem;
|
||||
|
||||
Reference in new issue
Block a user