<script lang="ts">
|
import { t } from '../../lib/i18n'
|
|
interface Summary {
|
new: number
|
csvDuplicate: number
|
dictDuplicate: number
|
conflict: number
|
invalid: number
|
empty: number
|
limit: number
|
}
|
|
interface PreviewItem {
|
row: number
|
slot: number
|
from: string
|
to: string
|
status: string
|
}
|
|
interface Preview {
|
previewId: string
|
fileName: string
|
summary: Summary
|
items: PreviewItem[]
|
totalSlots: number
|
visibleLimit: number
|
detailsTruncated: boolean
|
}
|
|
interface Props {
|
preview: Preview
|
confirming: boolean
|
oncancel: () => void
|
onconfirm: () => void
|
}
|
|
let { preview, confirming, oncancel, onconfirm }: Props = $props()
|
|
const summaryRows = [
|
{ key: 'new', label: () => t('userdict.previewWillAdd'), tone: 'new' },
|
{ key: 'csvDuplicate', label: () => t('userdict.previewCsvDuplicate'), tone: 'skip' },
|
{ key: 'dictDuplicate', label: () => t('userdict.previewDictDuplicate'), tone: 'skip' },
|
{ key: 'conflict', label: () => t('userdict.previewConflict'), tone: 'warn' },
|
{ key: 'invalid', label: () => t('userdict.previewInvalid'), tone: 'warn' },
|
{ key: 'empty', label: () => t('userdict.previewEmpty'), tone: 'skip' },
|
{ key: 'limit', label: () => t('userdict.previewLimitExceeded'), tone: 'warn' },
|
]
|
|
function countFor(key: string): number {
|
return (preview.summary as any)[key] || 0
|
}
|
|
function statusLabel(status: string): string {
|
switch (status) {
|
case 'new': return t('userdict.statusNew')
|
case 'csv_duplicate': return t('userdict.statusCsvDuplicate')
|
case 'dict_duplicate': return t('userdict.statusDictDuplicate')
|
case 'conflict': return t('userdict.statusConflict')
|
case 'invalid': return t('userdict.statusInvalid')
|
case 'empty': return t('userdict.statusEmpty')
|
case 'limit': return t('userdict.statusLimit')
|
default: return status
|
}
|
}
|
</script>
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<div class="overlay" onclick={oncancel} role="dialog" tabindex="-1">
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
<div class="dialog" onclick={(e) => e.stopPropagation()} role="document">
|
<div class="header">
|
<div>
|
<h2>{t('userdict.previewTitle')}</h2>
|
<p>{t('userdict.previewFile', { file: preview.fileName })}</p>
|
</div>
|
<button class="close-btn" onclick={oncancel} disabled={confirming} aria-label={t('userdict.cancel')}>×</button>
|
</div>
|
|
<div class="summary-grid">
|
{#each summaryRows as row}
|
<div class="summary-card" class:is-new={row.tone === 'new'} class:warn={row.tone === 'warn'}>
|
<span class="summary-label">{row.label()}</span>
|
<strong>{countFor(row.key)}</strong>
|
</div>
|
{/each}
|
</div>
|
|
{#if preview.detailsTruncated}
|
<p class="notice">{t('userdict.previewTruncated', { count: String(preview.visibleLimit) })}</p>
|
{/if}
|
|
<div class="details">
|
{#if preview.items.length === 0}
|
<p class="empty">{t('userdict.previewNoDetails')}</p>
|
{:else}
|
{#each preview.items as item}
|
<div class="detail-row">
|
<span class="row-index">{t('userdict.previewRowSlot', { row: String(item.row), slot: String(item.slot) })}</span>
|
<span class="rule-text" title={`${item.from} → ${item.to}`}>
|
{item.from || '—'} <span>→</span> {item.to || '—'}
|
</span>
|
<span class="status-pill" class:is-new={item.status === 'new'} class:warn={item.status === 'conflict' || item.status === 'invalid' || item.status === 'limit'}>
|
{statusLabel(item.status)}
|
</span>
|
</div>
|
{/each}
|
{/if}
|
</div>
|
|
{#if preview.summary.new === 0}
|
<p class="notice warning">{t('userdict.previewNoNewRules')}</p>
|
{/if}
|
|
<div class="actions">
|
<button class="btn secondary" onclick={oncancel} disabled={confirming}>{t('userdict.cancel')}</button>
|
<button class="btn primary" onclick={onconfirm} disabled={confirming || preview.summary.new === 0}>
|
{confirming ? t('userdict.confirmingImport') : t('userdict.confirmImport')}
|
</button>
|
</div>
|
</div>
|
</div>
|
|
<style>
|
.overlay {
|
position: fixed;
|
inset: 0;
|
z-index: 10000;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
background: rgba(0, 0, 0, 0.35);
|
backdrop-filter: blur(4px);
|
}
|
|
.dialog {
|
width: min(720px, 92vw);
|
max-height: 86vh;
|
display: flex;
|
flex-direction: column;
|
padding: var(--spacing-xl);
|
border-radius: var(--radius-lg);
|
background: var(--color-bg-primary);
|
box-shadow: 0 12px 44px rgba(0, 0, 0, 0.18);
|
}
|
|
.header,
|
.actions {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
gap: var(--spacing-md);
|
}
|
|
h2 {
|
font-size: var(--font-size-xl);
|
font-weight: 700;
|
}
|
|
p {
|
font-size: var(--font-size-sm);
|
color: var(--color-secondary-label);
|
}
|
|
.close-btn {
|
width: 28px;
|
height: 28px;
|
border: none;
|
border-radius: 50%;
|
color: var(--color-secondary-label);
|
background: transparent;
|
font-size: 22px;
|
line-height: 1;
|
}
|
|
.close-btn:hover {
|
color: var(--color-label);
|
background: var(--color-bg-secondary);
|
}
|
|
.summary-grid {
|
display: grid;
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
gap: 8px;
|
margin: var(--spacing-lg) 0 var(--spacing-sm);
|
}
|
|
.summary-card {
|
min-width: 0;
|
padding: 10px 12px;
|
border-radius: var(--radius-sm);
|
background: var(--color-bg-secondary);
|
}
|
|
.summary-card.is-new {
|
background: rgba(52, 199, 89, 0.12);
|
}
|
|
.summary-card.warn {
|
background: rgba(255, 149, 0, 0.12);
|
}
|
|
.summary-label {
|
display: block;
|
font-size: var(--font-size-xs);
|
color: var(--color-secondary-label);
|
white-space: nowrap;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
}
|
|
.summary-card strong {
|
display: block;
|
margin-top: 4px;
|
font-size: 20px;
|
}
|
|
.notice {
|
margin-top: 6px;
|
font-size: var(--font-size-xs);
|
color: var(--color-secondary-label);
|
}
|
|
.notice.warning {
|
color: var(--color-orange);
|
}
|
|
.details {
|
min-height: 120px;
|
overflow: auto;
|
margin-top: var(--spacing-md);
|
border: 1px solid var(--color-separator);
|
border-radius: var(--radius-sm);
|
}
|
|
.detail-row {
|
display: grid;
|
grid-template-columns: 92px minmax(0, 1fr) 96px;
|
gap: var(--spacing-sm);
|
align-items: center;
|
min-height: 36px;
|
padding: 7px 10px;
|
}
|
|
.detail-row + .detail-row {
|
border-top: 1px solid var(--color-separator);
|
}
|
|
.row-index {
|
font-size: var(--font-size-xs);
|
color: var(--color-tertiary-label);
|
}
|
|
.rule-text {
|
min-width: 0;
|
overflow: hidden;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
font-size: var(--font-size-sm);
|
}
|
|
.rule-text span {
|
color: var(--color-tertiary-label);
|
}
|
|
.status-pill {
|
justify-self: end;
|
padding: 2px 8px;
|
border-radius: 999px;
|
font-size: var(--font-size-xs);
|
color: var(--color-secondary-label);
|
background: var(--color-bg-secondary);
|
}
|
|
.status-pill.is-new {
|
color: var(--color-green);
|
background: rgba(52, 199, 89, 0.12);
|
}
|
|
.status-pill.warn {
|
color: var(--color-orange);
|
background: rgba(255, 149, 0, 0.12);
|
}
|
|
.empty {
|
padding: var(--spacing-xl);
|
text-align: center;
|
}
|
|
.actions {
|
justify-content: flex-end;
|
margin-top: var(--spacing-lg);
|
}
|
|
.btn {
|
height: 32px;
|
padding: 0 16px;
|
border: none;
|
border-radius: var(--radius-sm);
|
font-size: var(--font-size-sm);
|
font-weight: 600;
|
}
|
|
.btn.secondary {
|
color: var(--color-label);
|
background: var(--color-bg-secondary);
|
}
|
|
.btn.primary {
|
color: white;
|
background: var(--color-blue);
|
}
|
|
button {
|
font: inherit;
|
cursor: pointer;
|
}
|
|
button:disabled {
|
cursor: default;
|
opacity: 0.45;
|
}
|
</style>
|