feat(ui): redesign the version management page

This commit is contained in:
lzt404 committed 2026-08-26 01:56:46 +08:00
1 parent 3b9e3b9fd4
commit 6462699a3d
2 files changed
+462 -176

No files matched your search

+161 -111
View File
@@ -2,7 +2,23 @@ import { useEffect, useRef, useState } from 'react';
import { getVersion } from '@tauri-apps/api/app';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { AlertCircle, Check, Copy, ExternalLink, Eye, EyeOff, Info } from 'lucide-react';
import {
AlertCircle,
AppWindow,
ArrowRight,
Box,
Check,
Copy,
Database,
Download,
ExternalLink,
Eye,
EyeOff,
Info,
PackageOpen,
RefreshCw,
RotateCcw,
} from 'lucide-react';
import { type CoreStatus, useCoreRuntime } from '../coreRuntime';
import openaiIcon from '../assets/icons/openai-light.svg';
import claudeIcon from '../assets/icons/claude.svg';
@@ -668,6 +684,22 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
const currentAppVersion = resolvedAppVersion
? displayAppVersion(resolvedAppVersion)
: t('common.detecting');
const appVersionStatusLabel = appUpdateTask.running
? t(`appUpdate.phase.${appUpdateTask.phase}` as Parameters<typeof t>[0])
: appUpdateError
? t('kernel.update.failed')
: appUpdate?.updateAvailable
? t('appUpdate.available', { version: displayAppVersion(appUpdate.latestVersion) })
: appUpdate
? t('appUpdate.upToDate')
: t('appUpdate.phase.checking');
const appVersionStatusTone = appUpdateError
? 'error'
: appUpdate?.updateAvailable
? 'update'
: appUpdate
? 'success'
: '';
const latestLabel = checkingLatest
? t('kernel.update.checking')
: latestVersion || (latestError ? t('kernel.update.failed') : t('kernel.update.notChecked'));
@@ -746,6 +778,51 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
return (
<section className={`page kernel-page ${view === 'home' ? 'home-page' : 'version-management-page'}`}>
{view === 'versions' ? (
<header className="version-page-header">
<div className="version-page-title">
<span className="version-page-title-icon" aria-hidden="true">
<PackageOpen size={22} />
</span>
<div>
<h1>{t('app.nav.versions')}</h1>
<p>{t('kernel.versions.offlineHint')}</p>
</div>
</div>
<div className="version-source-control">
<div className="version-source-copy">
<Info size={17} aria-hidden="true" />
<div>
<strong>{t('kernel.versions.gitcodeSource')}</strong>
<span>
{versionSource?.gitcodeAvailable === false
? t('kernel.versions.gitcodeUnavailable')
: t('kernel.versions.gitcodeSourceHint')}
</span>
</div>
</div>
<label className="switch-control" title={t('kernel.versions.gitcodeSource')}>
<input
type="checkbox"
role="switch"
checked={versionSource?.preferGitcodeDownloads ?? false}
disabled={
(!versionSource?.gitcodeAvailable && !versionSource?.preferGitcodeDownloads)
|| versionSourceSaving
|| appUpdateTask.running
|| installing
}
aria-label={t('kernel.versions.gitcodeSource')}
onChange={(event) => void updateVersionSource(event.currentTarget.checked)}
/>
<span className="switch-track" />
</label>
{versionSourceError ? <p className="error version-source-error" role="alert">{versionSourceError}</p> : null}
</div>
</header>
) : null}
<div className={view === 'home' ? 'kernel-layout home-layout' : 'kernel-layout version-management-layout'}>
{view === 'home' ? (
<div className="panel control-panel">
@@ -822,85 +899,36 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
) : null}
{view === 'versions' ? (
<div className="panel version-source-panel">
<div className="version-source-copy">
<Info size={18} aria-hidden="true" />
<div>
<strong>{t('kernel.versions.gitcodeSource')}</strong>
<span>
{versionSource?.gitcodeAvailable === false
? t('kernel.versions.gitcodeUnavailable')
: t('kernel.versions.gitcodeSourceHint')}
<article className="panel version-product-card software-update-panel">
<div className="version-product-heading">
<div className="version-product-identity">
<span className="version-product-icon software" aria-hidden="true">
<AppWindow size={21} />
</span>
</div>
</div>
<label className="switch-control" title={t('kernel.versions.gitcodeSource')}>
<input
type="checkbox"
role="switch"
checked={versionSource?.preferGitcodeDownloads ?? false}
disabled={
(!versionSource?.gitcodeAvailable && !versionSource?.preferGitcodeDownloads)
|| versionSourceSaving
|| appUpdateTask.running
|| installing
}
aria-label={t('kernel.versions.gitcodeSource')}
onChange={(event) => void updateVersionSource(event.currentTarget.checked)}
/>
<span className="switch-track" />
</label>
{versionSourceError ? <p className="error version-source-error" role="alert">{versionSourceError}</p> : null}
</div>
) : null}
{view === 'versions' ? (
<div className="panel software-update-panel">
<div className="panel-heading">
<div>
<h2>{t('appUpdate.title')}</h2>
<p className={appUpdateError ? 'error' : appUpdate?.updateAvailable ? 'success' : ''}>
{appUpdateError
|| (appUpdate?.updateAvailable
? t('appUpdate.available', { version: displayAppVersion(appUpdate.latestVersion) })
: appUpdate
? t('appUpdate.upToDate')
: t('appUpdate.phase.checking'))}
</p>
</div>
{!appUpdate?.autoUpdateSupported && (
<span className={`state-pill ${appUpdateError ? 'error' : 'success'}`}>
{t('appUpdate.manualFallback')}
</span>
)}
<span className={`state-pill ${appVersionStatusTone}`} title={appUpdateError || appVersionStatusLabel}>
{appVersionStatusLabel}
</span>
</div>
<dl className="panel-detail-grid software-update-details">
<div className="panel-detail-row">
<dt>{t('appUpdate.current')}</dt>
<dd>{currentAppVersion}</dd>
<div className="version-comparison" aria-label={t('appUpdate.title')}>
<div className="version-value-block current">
<span>{t('appUpdate.current')}</span>
<strong>{currentAppVersion}</strong>
</div>
<div className="panel-detail-row">
<dt>{t('appUpdate.latest')}</dt>
<dd>{appUpdate ? displayAppVersion(appUpdate.latestVersion) : t('common.detecting')}</dd>
<ArrowRight className="version-comparison-arrow" size={20} aria-hidden="true" />
<div className="version-value-block latest">
<span>{t('appUpdate.latest')}</span>
<strong>{appUpdate ? displayAppVersion(appUpdate.latestVersion) : t('common.detecting')}</strong>
</div>
<div className="panel-detail-row">
<dt>{t('appUpdate.status')}</dt>
<dd className={appUpdateError ? 'error' : appUpdate?.updateAvailable ? 'success' : ''}>
{appUpdateTask.running
? t(`appUpdate.phase.${appUpdateTask.phase}` as Parameters<typeof t>[0])
: appUpdateError
? t('kernel.update.failed')
: appUpdate?.updateAvailable
? t('appUpdate.available', { version: displayAppVersion(appUpdate.latestVersion) })
: appUpdate
? t('appUpdate.upToDate')
: t('appUpdate.phase.checking')}
</dd>
</div>
<div className="panel-detail-row">
<dt>{t('appUpdate.catalog.label')}</dt>
<dd
</div>
<div className="version-card-support">
<Database size={17} aria-hidden="true" />
<div>
<strong>{t('appUpdate.catalog.label')}</strong>
<span
className={catalogUpdateError ? 'error' : catalogUpdateNotice ? 'success' : ''}
role={catalogUpdateError ? 'alert' : undefined}
aria-live="polite"
@@ -908,9 +936,17 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
{catalogUpdateError
|| catalogUpdateNotice
|| t('appUpdate.catalog.description')}
</dd>
</span>
</div>
</dl>
</div>
<div className="version-card-note-slot">
{appUpdateError ? (
<p className="version-card-note error" role="alert">{appUpdateError}</p>
) : !appUpdate?.autoUpdateSupported ? (
<p className="version-card-note">{t('appUpdate.manualFallback')}</p>
) : null}
</div>
<div className="button-row panel-action-row software-update-actions">
<button
@@ -919,6 +955,7 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
disabled={checkingAppUpdate || appUpdateTask.running}
onClick={() => void checkAppUpdate()}
>
<RefreshCw size={15} aria-hidden="true" />
{checkingAppUpdate ? t('appUpdate.checking') : t('appUpdate.check')}
</button>
{appUpdate?.updateAvailable && appUpdate.autoUpdateSupported ? (
@@ -928,10 +965,15 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
disabled={appUpdateTask.running}
onClick={requestAppUpdate}
>
<Download size={15} aria-hidden="true" />
{t('appUpdate.installNow')}
</button>
) : (
<button type="button" className="secondary-button" onClick={() => void openAppRelease()}>
<button
type="button"
className={appUpdate?.updateAvailable ? 'primary-button' : 'secondary-button'}
onClick={() => void openAppRelease()}
>
{t('appUpdate.openRelease')} <ExternalLink size={14} aria-hidden="true" />
</button>
)}
@@ -942,51 +984,55 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
onClick={() => void updateCodexModelCatalog()}
title={t('appUpdate.catalog.updateHint')}
>
<Database size={15} aria-hidden="true" />
{catalogUpdating
? t('appUpdate.catalog.updating')
: t('appUpdate.catalog.update')}
</button>
</div>
</div>
</article>
) : null}
{view === 'versions' ? (
<div className="panel version-panel">
<div className="panel-heading">
<div className="version-heading-inline">
<h2>{t('kernel.versions.title')}</h2>
<span className="version-offline-hint">
{t('kernel.versions.offlineHint')}
<article className="panel version-product-card version-panel">
<div className="version-product-heading">
<div className="version-product-identity">
<span className="version-product-icon core" aria-hidden="true">
<Box size={21} />
</span>
<h2>{t('kernel.versions.title')}</h2>
</div>
<span className={`state-pill ${versionStatusTone}`} title={versionStatusLabel}>
{versionStatusLabel}
</span>
</div>
<div className="version-core-values">
<div className="version-value-block current">
<span>{t('kernel.versions.current')}</span>
<strong>{currentVersion || t('kernel.status.notInstalled')}</strong>
</div>
<div className="version-value-block latest">
<span>{t('kernel.versions.latest')}</span>
<strong>{latestLabel}</strong>
</div>
<div className="version-value-block bundled">
<span>{t('kernel.versions.bundled')}</span>
<strong title={bundledCoreError || bundledCore?.assetName}>
{bundledCore?.version ?? (bundledCoreError ? t('common.detectionFailed') : t('kernel.versions.notIncluded'))}
</strong>
</div>
</div>
<dl className="panel-detail-grid">
<div className="panel-detail-row">
<dt>{t('kernel.versions.current')}</dt>
<dd>{currentVersion || t('kernel.status.notInstalled')}</dd>
<div className="version-card-support core-platform-line">
<Box size={17} aria-hidden="true" />
<div>
<strong>{t('kernel.versions.platform')}</strong>
<span title={platformError || undefined}>{platformOsLabel} / {platformArchLabel}</span>
</div>
<div className="panel-detail-row">
<dt>{t('kernel.versions.latest')}</dt>
<dd>{latestLabel}</dd>
</div>
<div className="panel-detail-row">
<dt>{t('kernel.versions.bundled')}</dt>
<dd title={bundledCoreError || bundledCore?.assetName}>
{bundledCore?.version ?? (bundledCoreError ? t('common.detectionFailed') : t('kernel.versions.notIncluded'))}
</dd>
</div>
<div className="panel-detail-row">
<dt>{t('kernel.versions.platform')}</dt>
<dd title={platformError || undefined}>{platformOsLabel} / {platformArchLabel}</dd>
</div>
<div className="panel-detail-row">
<dt>{t('kernel.versions.updateStatus')}</dt>
<dd className={versionStatusTone} title={versionStatusLabel}>
{versionStatusLabel}
</dd>
</div>
</dl>
</div>
<div className="version-card-note-slot" aria-hidden="true" />
<div className="button-row panel-action-row version-action-row">
<button
@@ -995,15 +1041,17 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
disabled={busy}
onClick={() => void checkLatest()}
>
<RefreshCw size={15} aria-hidden="true" />
{checkingLatest ? t('kernel.update.checking') : t('kernel.versions.check')}
</button>
<button
type="button"
className="secondary-button"
className={latestVersion && currentVersion !== latestVersion ? 'primary-button' : 'secondary-button'}
title={latestVersion ? t('kernel.versions.stopAndUpdateVersion', { version: latestVersion }) : t('kernel.versions.installLatest')}
disabled={!latestVersion || busy}
onClick={() => void stopAndInstallLatest()}
>
<Download size={15} aria-hidden="true" />
{t('kernel.versions.installLatest')}
</button>
<button
@@ -1013,20 +1061,22 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
disabled={!currentVersion || installDisabled}
onClick={() => installVersion(currentVersion)}
>
<RotateCcw size={15} aria-hidden="true" />
{t('kernel.versions.reinstall')}
</button>
<button
type="button"
className="primary-button"
className={offlineInstallRequired ? 'primary-button' : 'secondary-button'}
title={(bundledCore?.assetName ?? bundledCoreError) || t('kernel.versions.noBundled')}
disabled={!bundledCore || offlineInstallDisabled}
onClick={() => void installBundledCore()}
>
<PackageOpen size={15} aria-hidden="true" />
{t('kernel.versions.offlineInstall')}
</button>
</div>
</div>
</article>
) : null}
</div>
+301 -65
View File
@@ -1011,6 +1011,69 @@ textarea:focus-visible {
gap: 18px;
}
.version-management-page {
gap: 20px;
}
.version-page-header {
display: grid;
grid-template-columns: minmax(0, 1fr);
align-items: center;
gap: 24px;
min-width: 0;
padding: 22px 24px;
border: 1px solid var(--theme-e3e1db);
border-radius: 14px;
background:
linear-gradient(135deg, color-mix(in srgb, var(--ui-accent-soft) 62%, transparent), transparent 48%),
var(--theme-fffdf8);
box-shadow: var(--ui-panel-shadow);
}
.version-page-title {
display: flex;
min-width: 0;
align-items: center;
gap: 14px;
}
.version-page-title-icon,
.version-product-icon {
display: grid;
flex: none;
place-items: center;
color: var(--ui-accent-strong);
background: var(--ui-accent-soft);
}
.version-page-title-icon {
width: 46px;
height: 46px;
border: 1px solid color-mix(in srgb, var(--ui-accent) 22%, transparent);
border-radius: 13px;
}
.version-page-title > div {
min-width: 0;
}
.version-page-title h1 {
margin: 0;
color: var(--theme-2d2a26);
font-size: 24px;
line-height: var(--line-height-tight);
}
.version-page-title p {
margin: 5px 0 0;
overflow: hidden;
color: var(--theme-7a746c);
font-size: var(--font-size-meta);
line-height: var(--line-height-body);
text-overflow: ellipsis;
white-space: nowrap;
}
.config-subpage-panel {
display: grid;
min-width: 0;
@@ -1025,14 +1088,16 @@ textarea:focus-visible {
grid-column: auto;
}
.version-source-panel {
display: flex;
min-height: 68px;
flex-wrap: wrap;
.version-source-control {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
justify-content: space-between;
gap: 20px;
padding-block: 14px;
gap: 12px 18px;
min-width: 0;
padding: 12px 14px;
border: 1px solid color-mix(in srgb, var(--ui-accent) 17%, var(--theme-e3e1db));
border-radius: 10px;
background: color-mix(in srgb, var(--theme-fffdf8) 82%, var(--ui-accent-soft));
}
.version-source-copy {
@@ -1044,6 +1109,8 @@ textarea:focus-visible {
.version-source-copy > svg {
flex: none;
align-self: flex-start;
margin-top: 2px;
color: var(--ui-accent);
}
@@ -1055,45 +1122,200 @@ textarea:focus-visible {
.version-source-copy strong {
color: var(--theme-39352f);
font-size: var(--font-size-body);
font-size: var(--font-size-label);
}
.version-source-copy span {
display: -webkit-box;
overflow: hidden;
color: var(--theme-7a746c);
font-size: var(--font-size-meta);
line-height: var(--line-height-body);
font-size: var(--font-size-caption);
line-height: 1.4;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.version-source-error {
width: 100%;
grid-column: 1 / -1;
margin: 0;
font-size: var(--font-size-meta);
}
.version-heading-inline {
.version-product-card {
display: grid;
grid-template-rows: auto auto auto minmax(0, 1fr) auto;
align-content: start;
gap: 18px;
padding: 26px;
overflow: hidden;
}
.version-product-heading {
display: flex;
min-width: 0;
align-items: baseline;
gap: 10px;
align-items: center;
justify-content: space-between;
gap: 14px;
}
.version-heading-inline h2 {
.version-product-identity {
display: flex;
min-width: 0;
align-items: center;
gap: 11px;
}
.version-product-icon {
width: 38px;
height: 38px;
border-radius: 10px;
}
.version-product-icon.core {
color: var(--theme-554075);
background: var(--theme-f1ecfb);
}
.version-product-identity h2 {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.version-product-heading .state-pill {
min-width: 0;
max-width: 52%;
flex: none;
overflow: hidden;
text-overflow: ellipsis;
}
.version-offline-hint {
.version-comparison {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
gap: 16px;
min-height: 118px;
padding: 20px;
border: 1px solid var(--theme-e3e1db);
border-radius: 12px;
background: var(--theme-f4f1ea);
}
.version-comparison-arrow {
color: var(--theme-9a958d);
}
.version-value-block {
display: grid;
min-width: 0;
gap: 7px;
}
.version-value-block span {
color: var(--theme-8a847c);
font-size: var(--font-size-meta);
font-weight: 700;
}
.version-value-block strong {
min-width: 0;
overflow: hidden;
color: var(--theme-8a847c);
font-size: var(--font-size-caption);
font-weight: 400;
color: var(--theme-2d2a26);
font-size: var(--font-size-metric-compact);
line-height: var(--line-height-tight);
text-overflow: ellipsis;
white-space: nowrap;
}
.kernel-layout > .version-panel > .panel-heading {
padding-bottom: 10px;
.version-value-block.latest strong {
color: var(--ui-accent-strong);
}
.version-core-values {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
min-height: 118px;
border: 1px solid var(--theme-e3e1db);
border-radius: 12px;
background: var(--theme-f4f1ea);
}
.version-core-values .version-value-block {
align-content: center;
padding: 18px;
}
.version-core-values .version-value-block + .version-value-block {
border-left: 1px solid var(--theme-e3e1db);
}
.version-core-values .version-value-block strong {
font-size: 20px;
}
.version-card-support {
display: flex;
min-width: 0;
align-items: center;
gap: 11px;
min-height: 58px;
padding: 11px 13px;
border: 1px solid var(--theme-e3e1db);
border-radius: 10px;
background: var(--theme-faf8f2);
}
.version-card-support > svg {
flex: none;
color: var(--theme-7a746c);
}
.version-card-support > div {
display: grid;
min-width: 0;
gap: 2px;
}
.version-card-support strong,
.version-card-support span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.version-card-support strong {
color: var(--theme-39352f);
font-size: var(--font-size-meta);
}
.version-card-support span {
color: var(--theme-7a746c);
font-size: var(--font-size-caption);
}
.version-card-support span.success {
color: var(--theme-2f6b3f);
}
.version-card-support span.error {
color: var(--theme-9f2d20);
}
.version-card-note {
align-self: start;
margin: 0;
color: var(--theme-7a746c);
font-size: var(--font-size-caption);
line-height: var(--line-height-body);
}
.version-card-note-slot {
min-height: 0;
}
.version-card-note.error {
color: var(--theme-9f2d20);
}
.client-api-panel {
@@ -1295,6 +1517,14 @@ textarea:focus-visible {
grid-template-rows: repeat(3, auto);
}
.version-management-layout > .version-product-card {
grid-template-rows: auto auto auto minmax(0, 1fr) auto;
}
.version-management-layout > .version-product-card > .panel-action-row {
padding-top: 0;
}
.kernel-layout > .panel > .panel-heading {
min-height: 0;
height: auto;
@@ -1399,10 +1629,10 @@ textarea:focus-visible {
}
.button-row.version-action-row {
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-auto-flow: row;
grid-auto-columns: unset;
grid-auto-rows: 38px;
grid-auto-rows: 40px;
align-items: stretch;
gap: 10px;
}
@@ -1421,30 +1651,23 @@ textarea:focus-visible {
white-space: normal;
}
.software-update-details {
align-content: start;
}
.software-update-details .panel-detail-row {
grid-template-columns: 120px minmax(0, 1fr);
}
.software-update-details .panel-detail-row dt {
white-space: nowrap;
}
.software-update-actions {
grid-template-columns: repeat(auto-fit, minmax(min(120px, 100%), 1fr));
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-auto-flow: row;
grid-auto-rows: 38px;
grid-auto-rows: 40px;
align-items: stretch;
}
.software-update-actions > button:last-child:nth-child(3) {
grid-column: 1 / -1;
}
.software-update-actions > button,
.version-action-row > button {
width: 100%;
height: 38px;
min-height: 38px;
height: 40px;
min-height: 40px;
gap: 7px;
padding: 0 10px;
line-height: normal;
overflow-wrap: normal;
@@ -1452,11 +1675,11 @@ textarea:focus-visible {
white-space: nowrap;
}
.software-update-panel .panel-heading p {
white-space: normal;
}
@container workspace (min-width: 840px) {
.version-page-header {
grid-template-columns: minmax(260px, 0.8fr) minmax(360px, 1.2fr);
}
.home-page {
grid-template-columns: minmax(320px, 0.8fr) minmax(0, 1.2fr);
grid-auto-rows: 1fr;
@@ -1502,45 +1725,58 @@ textarea:focus-visible {
.version-management-layout {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: auto minmax(0, 1fr);
align-items: stretch;
}
.version-management-layout > .version-source-panel {
grid-column: 1 / -1;
grid-row: 1;
}
.version-management-layout > .panel {
height: 100%;
align-self: stretch;
grid-template-rows: auto minmax(0, 1fr) auto;
row-gap: clamp(12px, 2vh, 22px);
}
.version-management-layout > .version-source-panel {
height: auto;
grid-template-rows: none;
row-gap: 8px;
}
.version-management-layout > .software-update-panel,
.version-management-layout > .version-panel {
grid-row: 2;
}
.home-page > .home-layout > .control-panel > .panel-detail-grid,
.version-management-layout > .panel > .panel-detail-grid,
.home-page > .client-api-panel > .client-api-grid {
align-content: space-evenly;
}
.home-page > .home-layout > .control-panel > .panel-action-row,
.version-management-layout > .panel > .panel-action-row {
.home-page > .home-layout > .control-panel > .panel-action-row {
padding-top: 0;
}
}
@container workspace (max-width: 700px) {
.version-page-header {
gap: 18px;
}
.version-page-title p {
overflow: visible;
text-overflow: clip;
white-space: normal;
}
.version-product-heading {
align-items: flex-start;
flex-direction: column;
}
.version-product-heading .state-pill {
max-width: 100%;
}
.version-core-values {
grid-template-columns: minmax(0, 1fr);
}
.version-core-values .version-value-block {
padding: 14px 18px;
}
.version-core-values .version-value-block + .version-value-block {
border-top: 1px solid var(--theme-e3e1db);
border-left: 0;
}
}
.info-list {
display: grid;
gap: 10px;