This commit is contained in:
kura 2026-04-28 18:33:11 +08:00
parent 4798647b40
commit c855cf5be7
6 changed files with 331 additions and 132 deletions

View File

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CrossSubtitle-AI</title> <title>CrossSubtitle-AI</title>
</head> </head>
<body class="bg-slate-950"> <body>
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>

View File

@ -240,12 +240,14 @@ async function handleExport(format: 'srt' | 'vtt' | 'ass') {
<div class="toolbar-main"> <div class="toolbar-main">
<div class="toolbar-title"> <div class="toolbar-title">
<strong>CrossSubtitle</strong> <strong>CrossSubtitle</strong>
<span>桌面字幕工作台</span>
<span class="credit-line"> <span class="credit-line">
作者<a href="https://kuraa.cc" target="_blank" rel="noreferrer">kuraa</a> gpt5.4 by <a href="https://kuraa.cc" target="_blank" rel="noreferrer">kuraa</a>
</span> </span>
</div> </div>
<div class="toolbar-actions"> <div class="toolbar-actions">
<button class="button secondary toggle-button" type="button" @click="showAdvanced = !showAdvanced">
{{ showAdvanced ? '收起' : '设置' }}
</button>
<button class="button" type="button" :disabled="pending" @click="handlePickFiles"> <button class="button" type="button" :disabled="pending" @click="handlePickFiles">
{{ pending ? '提交中...' : '添加任务' }} {{ pending ? '提交中...' : '添加任务' }}
</button> </button>
@ -254,7 +256,6 @@ async function handleExport(format: 'srt' | 'vtt' | 'ass') {
<div class="workspace-toolbar"> <div class="workspace-toolbar">
<div class="toolbar-group"> <div class="toolbar-group">
<span class="group-title">任务参数</span>
<div class="form-grid"> <div class="form-grid">
<label class="field"> <label class="field">
<span>模式</span> <span>模式</span>
@ -286,13 +287,10 @@ async function handleExport(format: 'srt' | 'vtt' | 'ass') {
<input v-model="bilingualOutput" type="checkbox" /> <input v-model="bilingualOutput" type="checkbox" />
<span>双语导出</span> <span>双语导出</span>
</label> </label>
<button class="button secondary toggle-button" type="button" @click="showAdvanced = !showAdvanced">
{{ showAdvanced ? '隐藏高级设置' : '显示高级设置' }}
</button>
</div> </div>
</div> </div>
<p class="feedback status-text">{{ feedback }}</p> <p v-if="feedback" class="feedback status-text">{{ feedback }}</p>
</div> </div>
<div v-if="showAdvanced" class="advanced-shell"> <div v-if="showAdvanced" class="advanced-shell">

View File

@ -7,6 +7,15 @@ const props = defineProps<{
logs: string[] logs: string[]
}>() }>()
const isProcessing = computed(() => {
if (!props.task) return false
return !['completed', 'failed'].includes(props.task.status)
})
const canExport = computed(() => {
return props.task?.status === 'completed' && (props.task.segments?.length ?? 0) > 0
})
const emit = defineEmits<{ const emit = defineEmits<{
save: [segment: SubtitleSegment] save: [segment: SubtitleSegment]
export: [format: 'srt' | 'vtt' | 'ass'] export: [format: 'srt' | 'vtt' | 'ass']
@ -44,19 +53,22 @@ function updateTranslatedText(segment: SubtitleSegment, value: string) {
{{ task ? `${segments.length} 条片段` : '选择左侧任务后开始查看' }} {{ task ? `${segments.length} 条片段` : '选择左侧任务后开始查看' }}
</p> </p>
</div> </div>
<div class="export-actions"> <div v-if="task" class="export-actions">
<button class="button secondary small" @click="emit('export', 'srt')">SRT</button> <button class="button secondary small" :disabled="!canExport" @click="emit('export', 'srt')">SRT</button>
<button class="button secondary small" @click="emit('export', 'vtt')">VTT</button> <button class="button secondary small" :disabled="!canExport" @click="emit('export', 'vtt')">VTT</button>
<button class="button secondary small" @click="emit('export', 'ass')">ASS</button> <button class="button secondary small" :disabled="!canExport" @click="emit('export', 'ass')">ASS</button>
</div> </div>
</div> </div>
<div v-if="!task" class="empty-state"> <div v-if="!task" class="empty-state">
选择任务后显示字幕 <p>选择任务后显示字幕</p>
<p style="margin-top: 6px; font-size: 11px;">点击左侧任务列表中的任务开始查看</p>
</div> </div>
<div v-else-if="segments.length === 0" class="empty-state"> <div v-else-if="segments.length === 0" class="empty-state">
暂无字幕片段 <template v-if="isProcessing">正在处理中请稍候...</template>
<template v-else-if="task?.status === 'failed'">任务处理失败无法生成字幕</template>
<template v-else>暂无字幕片段</template>
</div> </div>
<div v-else class="segment-list"> <div v-else class="segment-list">
@ -67,13 +79,14 @@ function updateTranslatedText(segment: SubtitleSegment, value: string) {
> >
<div class="task-row subtle"> <div class="task-row subtle">
<span>{{ formatTime(segment.start) }} - {{ formatTime(segment.end) }}</span> <span>{{ formatTime(segment.start) }} - {{ formatTime(segment.end) }}</span>
<span>{{ segment.id }}</span> <span class="segment-id">{{ segment.id }}</span>
</div> </div>
<p class="source-text">{{ segment.sourceText || '等待识别结果...' }}</p> <p class="source-text">{{ segment.sourceText || '等待识别结果...' }}</p>
<textarea <textarea
class="editor-input" class="editor-input"
:value="segment.translatedText ?? ''" :value="segment.translatedText ?? ''"
placeholder="译文" :placeholder="task.outputMode === 'translate' ? '译文' : '原文'"
:disabled="task.outputMode === 'source'"
@change="updateTranslatedText(segment, ($event.target as HTMLTextAreaElement).value)" @change="updateTranslatedText(segment, ($event.target as HTMLTextAreaElement).value)"
/> />
</article> </article>
@ -81,9 +94,9 @@ function updateTranslatedText(segment: SubtitleSegment, value: string) {
<div class="log-drawer" :class="{ expanded: logsExpanded }"> <div class="log-drawer" :class="{ expanded: logsExpanded }">
<button class="log-toggle" type="button" @click="logsExpanded = !logsExpanded"> <button class="log-toggle" type="button" @click="logsExpanded = !logsExpanded">
<span>运行日志</span> <span>日志</span>
<span class="subtle">{{ logs.length }} </span> <span class="subtle">{{ logs.length }}</span>
<span>{{ logsExpanded ? '收起' : '展开' }}</span> <span class="log-chevron">{{ logsExpanded ? '' : '+' }}</span>
</button> </button>
<div v-if="logsExpanded" class="log-panel"> <div v-if="logsExpanded" class="log-panel">
<div v-if="logs.length === 0" class="empty-state"> <div v-if="logs.length === 0" class="empty-state">

View File

@ -25,8 +25,8 @@ const statusLabel: Record<SubtitleTask['status'], string> = {
<aside class="panel sidebar-panel"> <aside class="panel sidebar-panel">
<div class="panel-title"> <div class="panel-title">
<div> <div>
<strong>任务队列</strong> <strong>任务</strong>
<p class="panel-subtitle">选择任务查看字幕和日志</p> <p class="panel-subtitle">选择任务查看字幕</p>
</div> </div>
<span class="badge">{{ tasks.length }}</span> <span class="badge">{{ tasks.length }}</span>
</div> </div>
@ -40,16 +40,18 @@ const statusLabel: Record<SubtitleTask['status'], string> = {
v-for="task in tasks" v-for="task in tasks"
:key="task.id" :key="task.id"
class="task-item" class="task-item"
:class="{ active: task.id === selectedTaskId }" :class="{
active: task.id === selectedTaskId,
completed: task.status === 'completed',
}"
@click="emit('select', task.id)" @click="emit('select', task.id)"
> >
<div class="task-row"> <div class="task-row">
<strong class="truncate">{{ task.fileName }}</strong> <strong class="truncate">{{ task.fileName }}</strong>
<span>{{ Math.round(task.progress) }}%</span> <span
</div> class="subtle"
<div class="task-row subtle"> :class="{ 'status-active': task.status !== 'completed' && task.status !== 'failed' && task.status !== 'queued' }"
<span>{{ statusLabel[task.status] }}</span> >{{ statusLabel[task.status] }}</span>
<span>{{ task.segments.length }} </span>
</div> </div>
<div class="progress"> <div class="progress">
<div class="progress-bar" :style="{ width: `${task.progress}%` }" /> <div class="progress-bar" :style="{ width: `${task.progress}%` }" />

View File

@ -3,10 +3,33 @@
@tailwind utilities; @tailwind utilities;
:root { :root {
color: #111827; --c-bg: #fafafa;
background: #eef1f5; --c-surface: #ffffff;
font-family: "PingFang SC", "Helvetica Neue", Arial, sans-serif; --c-border: #ebebeb;
line-height: 1.4; --c-border-hover: #d4d4d4;
--c-text: #1a1a2e;
--c-text-secondary: #6b6b7b;
--c-text-tertiary: #9999a8;
--c-accent: #1a1a2e;
--c-accent-hover: #2a2a3e;
--c-focus: rgba(26, 26, 46, 0.08);
--c-progress: #1a1a2e;
--c-error: #dc2626;
--c-log-bg: #1a1a2e;
--c-log-text: #d4d4d4;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--radius-full: 999px;
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.06);
--transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
color: var(--c-text);
background: var(--c-bg);
font-family: "PingFang SC", "Helvetica Neue", -apple-system, BlinkMacSystemFont, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
} }
html, html,
@ -30,26 +53,57 @@ textarea {
font: inherit; font: inherit;
} }
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--c-border);
border-radius: var(--radius-full);
border: 1px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
background: var(--c-border-hover);
border: 1px solid transparent;
background-clip: content-box;
}
::selection {
background: rgba(26, 26, 46, 0.1);
}
.app-shell { .app-shell {
width: 100%; width: 100%;
max-width: 1680px; max-width: 1600px;
margin: 0 auto; margin: 0 auto;
padding: 14px; padding: 20px;
height: 100vh; height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 14px; gap: 16px;
} }
.panel { .panel {
background: linear-gradient(180deg, #ffffff 0%, #fbfcfe 100%); background: var(--c-surface);
border: 1px solid #d9e0e8; border: 1px solid var(--c-border);
border-radius: 12px; border-radius: var(--radius-lg);
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05); box-shadow: var(--shadow-sm);
transition: box-shadow var(--transition);
}
.panel:hover {
box-shadow: var(--shadow-md);
} }
.topbar { .topbar {
padding: 14px 16px; padding: 20px 24px;
flex: 0 0 auto; flex: 0 0 auto;
} }
@ -60,7 +114,7 @@ textarea {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 10px; gap: 12px;
min-width: 0; min-width: 0;
} }
@ -76,8 +130,9 @@ textarea {
.panel-title strong, .panel-title strong,
.workspace-header strong { .workspace-header strong {
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 500;
color: #0f172a; color: var(--c-text);
letter-spacing: -0.01em;
} }
.toolbar-title span, .toolbar-title span,
@ -85,8 +140,9 @@ textarea {
.subtle, .subtle,
.feedback, .feedback,
.empty-state { .empty-state {
color: #64748b; color: var(--c-text-secondary);
font-size: 12px; font-size: 12px;
font-weight: 400;
} }
.credit-line { .credit-line {
@ -97,33 +153,37 @@ textarea {
} }
.credit-line a { .credit-line a {
color: #0f172a; color: var(--c-text);
text-decoration: none; text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color var(--transition);
} }
.credit-line a:hover { .credit-line a:hover {
text-decoration: underline; border-bottom-color: var(--c-text);
} }
.workspace-toolbar, .workspace-toolbar,
.advanced-shell { .advanced-shell {
margin-top: 12px; margin-top: 16px;
padding-top: 12px; padding-top: 16px;
border-top: 1px solid #e6ebf1; border-top: 1px solid var(--c-border);
} }
.group-title { .group-title {
display: inline-block; display: inline-block;
margin-bottom: 8px; margin-bottom: 10px;
color: #475569; color: var(--c-text-secondary);
font-size: 12px; font-size: 11px;
font-weight: 600; font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.06em;
} }
.form-grid, .form-grid,
.advanced-grid { .advanced-grid {
display: grid; display: grid;
gap: 10px; gap: 12px;
} }
.form-grid { .form-grid {
@ -139,12 +199,13 @@ textarea {
.check { .check {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 5px; gap: 6px;
font-size: 12px; font-size: 12px;
} }
.field span { .field span {
color: #475569; color: var(--c-text-secondary);
font-weight: 400;
} }
.field input, .field input,
@ -160,35 +221,53 @@ textarea {
.field select, .field select,
.editor-input { .editor-input {
width: 100%; width: 100%;
min-height: 34px; min-height: 36px;
padding: 6px 9px; padding: 8px 12px;
border: 1px solid #cfd8e3; border: 1px solid var(--c-border);
border-radius: 8px; border-radius: var(--radius-sm);
background: #fff; background: var(--c-surface);
color: #0f172a; color: var(--c-text);
transition: border-color var(--transition), box-shadow var(--transition);
}
.field input::placeholder,
.field select::placeholder,
.editor-input::placeholder {
color: var(--c-text-tertiary);
} }
.field input:focus, .field input:focus,
.field select:focus, .field select:focus,
.editor-input:focus { .editor-input:focus {
outline: none; outline: none;
border-color: #94a3b8; border-color: var(--c-accent);
box-shadow: 0 0 0 3px rgba(148, 163, 184, 0.16); box-shadow: 0 0 0 3px var(--c-focus);
} }
.check { .check {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 34px; gap: 8px;
padding: 0 10px; min-height: 36px;
border: 1px solid #d9e0e8; padding: 0 12px;
border-radius: 8px; border: 1px solid var(--c-border);
background: #f8fafc; border-radius: var(--radius-sm);
background: var(--c-bg);
cursor: pointer;
user-select: none;
transition: border-color var(--transition), background var(--transition);
}
.check:hover {
border-color: var(--c-border-hover);
background: var(--c-surface);
} }
.check input { .check input {
margin: 0; margin: 0;
accent-color: var(--c-accent);
cursor: pointer;
} }
.desktop-check { .desktop-check {
@ -200,50 +279,57 @@ textarea {
} }
.button { .button {
min-height: 34px; min-height: 36px;
padding: 0 12px; padding: 0 16px;
border: 1px solid #1f2937; border: 1px solid var(--c-accent);
border-radius: 8px; border-radius: var(--radius-sm);
background: #1f2937; background: var(--c-accent);
color: #fff; color: #fff;
cursor: pointer; cursor: pointer;
transition: background 0.15s ease, border-color 0.15s ease; font-weight: 400;
letter-spacing: 0.01em;
transition: background var(--transition), border-color var(--transition), transform 0.1s ease;
} }
.button:hover { .button:hover {
background: #111827; background: var(--c-accent-hover);
}
.button:active {
transform: scale(0.98);
} }
.button.secondary { .button.secondary {
background: #fff; background: transparent;
color: #111827; color: var(--c-text);
border-color: #cfd8e3; border-color: var(--c-border);
} }
.button.secondary:hover { .button.secondary:hover {
background: #f8fafc; background: var(--c-bg);
border-color: var(--c-border-hover);
} }
.button.small { .button.small {
min-height: 30px; min-height: 30px;
padding: 0 10px; padding: 0 12px;
font-size: 12px; font-size: 12px;
} }
.button:disabled { .button:disabled {
cursor: default; cursor: default;
opacity: 0.6; opacity: 0.4;
} }
.status-text { .status-text {
min-height: 18px; min-height: 18px;
margin: 10px 0 0; margin: 12px 0 0;
} }
.content-grid { .content-grid {
display: grid; display: grid;
grid-template-columns: 320px minmax(0, 1fr); grid-template-columns: 300px minmax(0, 1fr);
gap: 14px; gap: 16px;
min-height: 0; min-height: 0;
flex: 1 1 auto; flex: 1 1 auto;
} }
@ -256,31 +342,33 @@ textarea {
.sidebar-panel { .sidebar-panel {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 12px; padding: 16px;
} }
.workspace-panel { .workspace-panel {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 14px; padding: 20px;
} }
.badge { .badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-width: 26px; min-width: 24px;
height: 26px; height: 24px;
padding: 0 8px; padding: 0 8px;
border-radius: 999px; border-radius: var(--radius-full);
background: #e2e8f0; background: var(--c-bg);
color: #334155; color: var(--c-text-secondary);
font-size: 12px; font-size: 11px;
font-weight: 600; font-weight: 500;
border: 1px solid var(--c-border);
} }
.empty-state { .empty-state {
padding: 14px 0; padding: 20px 0;
color: var(--c-text-tertiary);
} }
.list-stack, .list-stack,
@ -305,16 +393,44 @@ textarea {
.task-item, .task-item,
.segment-item { .segment-item {
width: 100%; width: 100%;
padding: 10px; padding: 12px 14px;
border: 1px solid #e3e8ef; border: 1px solid var(--c-border);
border-radius: 10px; border-radius: var(--radius-md);
background: #f8fafc; background: var(--c-surface);
text-align: left; text-align: left;
cursor: pointer;
transition: border-color var(--transition), background var(--transition), box-shadow var(--transition);
}
.task-item:hover,
.segment-item:hover {
border-color: var(--c-border-hover);
box-shadow: var(--shadow-sm);
} }
.task-item.active { .task-item.active {
border-color: #94a3b8; border-color: var(--c-accent);
background: #eef2f7; background: rgba(26, 26, 46, 0.03);
box-shadow: inset 0 0 0 1px var(--c-accent);
}
.task-item.completed {
opacity: 0.85;
}
.task-item.completed::after {
content: '';
position: absolute;
top: 12px;
right: 12px;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--c-success, #2d6a4f);
}
.task-item {
position: relative;
} }
.truncate { .truncate {
@ -324,48 +440,57 @@ textarea {
} }
.progress { .progress {
height: 4px; height: 2px;
margin-top: 6px; margin-top: 8px;
background: #dbe4ee; background: var(--c-border);
border-radius: 999px; border-radius: var(--radius-full);
overflow: hidden; overflow: hidden;
} }
.progress-bar { .progress-bar {
height: 100%; height: 100%;
background: #334155; background: var(--c-progress);
transition: width 0.3s ease;
} }
.error-text { .error-text {
margin: 6px 0 0; margin: 8px 0 0;
color: #b91c1c; color: var(--c-error);
font-size: 12px; font-size: 12px;
} }
.export-actions { .export-actions {
display: flex; display: flex;
gap: 8px; gap: 6px;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-end; justify-content: flex-end;
} }
.source-text { .source-text {
margin: 8px 0; margin: 10px 0;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
font-size: 13px; font-size: 13px;
color: #0f172a; color: var(--c-text);
line-height: 1.6;
} }
.editor-input { .editor-input {
min-height: 64px; min-height: 64px;
resize: vertical; resize: vertical;
line-height: 1.5;
}
.editor-input:disabled {
background: var(--c-bg);
cursor: not-allowed;
opacity: 0.6;
} }
.log-drawer { .log-drawer {
margin-top: 12px; margin-top: 16px;
border-top: 1px solid #e6ebf1; border-top: 1px solid var(--c-border);
padding-top: 12px; padding-top: 16px;
} }
.log-toggle { .log-toggle {
@ -375,12 +500,18 @@ textarea {
grid-template-columns: 1fr auto auto; grid-template-columns: 1fr auto auto;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 0 12px; padding: 0 14px;
border: 1px solid #d9e0e8; border: 1px solid var(--c-border);
border-radius: 10px; border-radius: var(--radius-md);
background: #f8fafc; background: var(--c-surface);
color: #0f172a; color: var(--c-text);
cursor: pointer; cursor: pointer;
transition: border-color var(--transition), background var(--transition);
}
.log-toggle:hover {
border-color: var(--c-border-hover);
background: var(--c-bg);
} }
.log-panel { .log-panel {
@ -390,10 +521,10 @@ textarea {
.log-list { .log-list {
max-height: 220px; max-height: 220px;
overflow: auto; overflow: auto;
padding: 10px; padding: 14px;
border: 1px solid #dfe6ee; border: 1px solid var(--c-border);
border-radius: 10px; border-radius: var(--radius-md);
background: #0f172a; background: var(--c-log-bg);
} }
.log-line { .log-line {
@ -401,17 +532,72 @@ textarea {
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
font-size: 12px; font-size: 12px;
line-height: 1.5; line-height: 1.6;
color: #dbe4ee; color: var(--c-log-text);
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
} }
.log-line + .log-line { .log-line + .log-line {
margin-top: 4px; margin-top: 2px;
}
.segment-id {
color: var(--c-text-tertiary);
font-family: "SF Mono", "Fira Code", monospace;
font-size: 11px;
}
.log-chevron {
font-size: 14px;
font-weight: 300;
color: var(--c-text-secondary);
width: 18px;
height: 18px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.toggle-button {
font-size: 12px;
}
.toolbar-actions {
display: flex;
gap: 8px;
align-items: center;
}
.task-item strong {
font-weight: 500;
font-size: 13px;
}
.task-item .subtle {
margin-top: 2px;
}
.task-item .status-active {
color: var(--c-accent);
font-weight: 500;
}
.segment-item {
border-left: 3px solid transparent;
transition: border-color var(--transition), background var(--transition), box-shadow var(--transition);
}
.segment-item:hover {
border-left-color: var(--c-border-hover);
}
.segment-item .task-row {
margin-bottom: 4px;
} }
@media (max-width: 1360px) { @media (max-width: 1360px) {
.content-grid { .content-grid {
grid-template-columns: 300px minmax(0, 1fr); grid-template-columns: 280px minmax(0, 1fr);
} }
.form-grid { .form-grid {

View File

@ -4,14 +4,14 @@ export default {
theme: { theme: {
extend: { extend: {
colors: { colors: {
ink: '#0f172a', ink: '#1a1a2e',
mist: '#f8fafc', mist: '#fafafa',
ember: '#c2410c', ember: '#e85d04',
lagoon: '#0f766e', lagoon: '#2d6a4f',
haze: '#dbeafe', haze: '#f0f0f0',
}, },
boxShadow: { boxShadow: {
float: '0 24px 60px rgba(15, 23, 42, 0.18)', float: '0 8px 32px rgba(0, 0, 0, 0.08)',
}, },
fontFamily: { fontFamily: {
display: ['"Avenir Next"', '"PingFang SC"', 'sans-serif'], display: ['"Avenir Next"', '"PingFang SC"', 'sans-serif'],