diff --git a/VERSION b/VERSION index b7c0622b..52980677 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.74.0 +0.74.1 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6da71cdb..cf4dc4b2 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -8,6 +8,23 @@ This project follows the repository versioning rule: - `improvement` -> `+0.0.1`(bugfix + 小功能混合) - `bugfix` -> `+0.0.1` +## [0.74.1] — 2026-06-30 + +Released: 2026-06-30 + +### Highlights +- 将 `/earth-content` 的品牌标识上传收敛到 `Logo 地址` 与 `标题图地址` 字段内,移除旧的全局“选择资产/上传”工具栏。 +- 新增字段级图片拖拽反馈,拖到对应字段时直接提示将图片复制为 Logo 或标题图。 +- 对齐品牌上传按钮到现有 Tactile UI primary 按钮样式,并同步中英文使用手册、快速开始和控制台上下文文档。 + +### Added / Fixed / Improved +- `BrandAssetInput` 支持字段内选择文件、拖拽上传、单字段 loading 和上传后回写草稿 URL。 +- `FieldGrid` 支持按字段注入自定义输入控件,同时复用统一草稿提交路径。 +- 品牌上传拖拽态改为低饱和 tactile 配色,上传按钮保持蓝色轻立体样式,容器内上下/右侧留白对齐为 3px。 +- 补齐品牌上传相关 legacy UI 英文翻译、术语对照和用户文档。 + +--- + ## [0.74.0] — 2026-06-30 Released: 2026-06-30 diff --git a/docs/technical/en/frontend-admin-frontend-context.md b/docs/technical/en/frontend-admin-frontend-context.md index eabbe2dc..eb391b52 100644 --- a/docs/technical/en/frontend-admin-frontend-context.md +++ b/docs/technical/en/frontend-admin-frontend-context.md @@ -387,6 +387,7 @@ Current boundary: `/earth-content` reuses the same single-screen tab container from [Settings.tsx](/home/ray/dev/linkong/planet/frontend/src/admin/pages/PlainResourcePages.tsx), but its ownership is separate from System Settings: - `TV Livestream` owns the Earth media-panel source configuration. +- `Branding` owns Earth HUD brand assets. `Logo URL` and `Title Image URL` use inline upload controls inside the fields; the upload buttons keep the primary `TactileButton` style, and dropping an image onto the matching field shows a low-saturation drag reaction instead of the old global asset-picker toolbar. - `Boundary Precision` owns the Earth static boundary asset state: provider, low-precision fallback, high-precision manifest/PMTiles, source JSON, and build action. - `Base Map`, `Layer Resources`, `3D Assets`, and `News Anchor Strategy` are placeholders only. They show module status and do not invent fake APIs or fake data. diff --git a/docs/technical/en/manual.md b/docs/technical/en/manual.md index 3403666c..e3f6a39c 100644 --- a/docs/technical/en/manual.md +++ b/docs/technical/en/manual.md @@ -192,7 +192,7 @@ TV livestreams and boundary precision moved to `/earth-content`; collectors and `/earth-content` is under the console's Operations and Configuration group and owns resources used by the Earth frontend: -- **Brand Assets**: manages the logo, title image, title text, subtitle, and description used by the Earth HUD. Uploaded images are saved as Earth brand assets and read by the Earth page immediately. +- **Brand Assets**: manages the logo, title image, title text, subtitle, and description used by the Earth HUD. The `Logo URL` and `Title Image URL` fields each include their own Upload button, and image files can be dropped directly onto the matching field. After upload, the field receives the new asset URL; save the brand configuration to make the Earth page use it. - **About**: manages the About card shown in Earth settings, including logo, kicker, title, version, description, and metadata. - **TV Livestream**: manages sources shown in the Earth media panel. - **News Content**: browses news grouped by RSS source and manual group. RSS items remain read-only; manual groups support create, JSON import, edit, delete, and reprocess. diff --git a/docs/technical/en/naming-glossary.md b/docs/technical/en/naming-glossary.md index 734323c5..c27c0227 100644 --- a/docs/technical/en/naming-glossary.md +++ b/docs/technical/en/naming-glossary.md @@ -28,6 +28,9 @@ This document standardizes terms used across Intelligent Planet, the console, ba | AI Provider | AI Provider | Service name | | tool | 工具 | Web Search, OCR, and similar integrations | | Playground | Playground | Interactive debugging entry | +| branding | 品牌标识 | Earth HUD brand configuration section under `/earth-content` | +| brand assets | 品牌资源 | Logo, title image, and related HUD copy assets | +| title image | 标题图 | Earth HUD title image | ## Data Types diff --git a/docs/technical/en/quickstart.md b/docs/technical/en/quickstart.md index 345cb7ef..849f57e8 100644 --- a/docs/technical/en/quickstart.md +++ b/docs/technical/en/quickstart.md @@ -34,9 +34,10 @@ After landing on the `/admin` dashboard, here's a recommended walk-through: 1. `/collection-management?section=collector_credentials`: pick a collector and click the plug icon to test connectivity. Free collectors (e.g. open BGP) usually work right away; credential-bearing ones like `AISStream` or `BarentsWatch` need an API key / client secret first 2. `/ai?section=integrations`: fill an LLM provider (e.g. `minimax` / `openai`), model, base URL, API key, and click the plug at the end of the base URL to test. WebSearch / OCR tools are optional -3. `/datasources` or `/data`: check whether collectors have produced data. Use `/datasources -> Built-in Sources` for finite collectors: with no rows selected, click `Trigger All`; after selecting rows, the primary button becomes `Trigger Selected N`. The top-right queue button shows progress. Use `/datasources -> Realtime Sources` for AISStream / WebSocket health and counters -4. `/alerts/system`: verify system alerts look right -5. `/users` (super_admin only): open accounts for teammates or adjust their groups +3. `/earth-content?section=brand`: maintain the Earth HUD logo and title image in Branding. The Upload button inside each URL field opens a file picker, and image files can also be dropped directly onto the matching field. Save the brand configuration after upload +4. `/datasources` or `/data`: check whether collectors have produced data. Use `/datasources -> Built-in Sources` for finite collectors: with no rows selected, click `Trigger All`; after selecting rows, the primary button becomes `Trigger Selected N`. The top-right queue button shows progress. Use `/datasources -> Realtime Sources` for AISStream / WebSocket health and counters +5. `/alerts/system`: verify system alerts look right +6. `/users` (super_admin only): open accounts for teammates or adjust their groups ## 4. Open Earth diff --git a/docs/technical/zh/frontend-admin-frontend-context.md b/docs/technical/zh/frontend-admin-frontend-context.md index 5092d5c8..fd51d036 100644 --- a/docs/technical/zh/frontend-admin-frontend-context.md +++ b/docs/technical/zh/frontend-admin-frontend-context.md @@ -418,6 +418,7 @@ AI 配置不再挂在 `/settings` 下;`/playground` 应跳转到 `/ai?section= `/earth-content` 复用 [Settings.tsx](/home/ray/dev/linkong/planet/frontend/src/admin/pages/PlainResourcePages.tsx) 的单屏 tab 容器,但页面责任与系统设置分离: - `电视直播` 迁移原直播源配置,继续管理 Earth 媒体面板内容源。 +- `品牌标识` 管理 Earth HUD 的品牌资源。`Logo 地址` 与 `标题图地址` 使用字段内上传控件,上传按钮保持 `TactileButton` 的 primary 样式;图片拖到对应字段时显示低饱和拖拽反应,避免回到旧的全局“选择资产/上传”工具栏。 - `国界精度` 管理 Earth 静态国界资产:provider 状态、低精 fallback、高精 manifest/PMTiles、源配置 JSON 和构建动作。 - `地球底图`、`图层资源`、`三维素材`、`新闻锚点策略` 是占位页,只显示模块待接入,不造假接口或假数据。 diff --git a/docs/technical/zh/manual.md b/docs/technical/zh/manual.md index 80e26516..b1f22d31 100644 --- a/docs/technical/zh/manual.md +++ b/docs/technical/zh/manual.md @@ -191,7 +191,7 @@ Base URL 输入框尾端的插头图标会触发连接测试。测试通过会 `/earth-content` 位于控制台“运维与配置”下,面向智能星球前端体验资源: -- **品牌资源**:维护智能星球 HUD 使用的 logo、标题图、标题文本、副标题和描述;上传的图片会保存为智能星球品牌资产并立即供智能星球页面读取。 +- **品牌资源**:维护智能星球 HUD 使用的 logo、标题图、标题文本、副标题和描述。`Logo 地址` 和 `标题图地址` 字段内各有独立的“上传”按钮,也可以把图片直接拖到对应字段;上传成功后字段会写入新的资产地址,保存品牌配置后供智能星球页面读取。 - **关于**:维护智能星球设置面板里的关于卡片,包括 logo、眉标、标题、版本、描述和元信息。 - **电视直播**:维护智能星球媒体面板里的直播源。 - **新闻内容**:按 RSS 来源和手动新闻组查看新闻。RSS 新闻保持只读;手动新闻组可以新增、批量导入 JSON、编辑、删除和重新处理。 diff --git a/docs/technical/zh/naming-glossary.md b/docs/technical/zh/naming-glossary.md index fe524b4b..9f9be414 100644 --- a/docs/technical/zh/naming-glossary.md +++ b/docs/technical/zh/naming-glossary.md @@ -28,6 +28,9 @@ | AI Provider | AI Provider | 服务名,保留英文 | | tool | 工具 | Web Search、OCR 等工具配置 | | Playground | Playground | 交互调试入口,保留英文 | +| branding | 品牌标识 | `/earth-content` 中的 Earth HUD 品牌配置分区 | +| brand assets | 品牌资源 | Logo、标题图和相关 HUD 文案资源 | +| title image | 标题图 | Earth HUD 标题图片,不写作“标题图片地址”以外的混合名 | ## 数据类型 diff --git a/docs/technical/zh/quickstart.md b/docs/technical/zh/quickstart.md index cb4cffa4..f54a28df 100644 --- a/docs/technical/zh/quickstart.md +++ b/docs/technical/zh/quickstart.md @@ -34,9 +34,10 @@ 1. `/collection-management?section=collector_credentials`:选一个采集器,点插头图标做连接测试。免费 collector(开源 BGP 等)通常直接可用;像 `AISStream`、`BarentsWatch` 这类需要凭证的,需要先填 API Key/Client Secret 2. `/ai?section=integrations`:填一个 LLM provider(例如 `minimax` / `openai`)、模型名、Base URL、API Key,点 Base URL 末端的插头测试连接。WebSearch / OCR 工具可选 -3. `/datasources` 或 `/data`:看采集器是否已经产出数据。有限采集器看 `/datasources -> 内置源`,不勾选时点“触发全部”,勾选后主按钮会变成“触发已选 N”;右上角队列按钮可查看进度。AISStream / WebSocket 长连接看 `/datasources -> 实时源` 的健康状态和计数 -4. `/alerts/system`:看系统告警是否正常 -5. `/users`(仅 `super_admin`):根据需要给同事开账号或调权限组 +3. `/earth-content?section=brand`:在“品牌标识”里维护智能星球的 Logo 和标题图;对应地址字段内的“上传”按钮支持选择文件,也支持把图片直接拖到字段上,保存后会应用到智能星球 HUD +4. `/datasources` 或 `/data`:看采集器是否已经产出数据。有限采集器看 `/datasources -> 内置源`,不勾选时点“触发全部”,勾选后主按钮会变成“触发已选 N”;右上角队列按钮可查看进度。AISStream / WebSocket 长连接看 `/datasources -> 实时源` 的健康状态和计数 +5. `/alerts/system`:看系统告警是否正常 +6. `/users`(仅 `super_admin`):根据需要给同事开账号或调权限组 ## 4. 打开智能星球 diff --git a/docs/version-history.md b/docs/version-history.md index c92579cb..79c073b2 100644 --- a/docs/version-history.md +++ b/docs/version-history.md @@ -16,12 +16,13 @@ ## Current Version - `main` 当前主线历史推导到:`0.16.5` -- `dev` 当前开发分支历史推导到:`0.74.0` +- `dev` 当前开发分支历史推导到:`0.74.1` ## Timeline | Version | Type | Branch | Commit | Summary | | --- | --- | --- | --- | --- | +| `0.74.1` | improvement | `dev` | `pending` | 将品牌标识上传收敛到 Logo/标题图字段内,新增字段级拖拽反馈和 Tactile UI primary 上传按钮,并同步中英文使用文档 | | `0.74.0` | feature | `dev` | `pending` | 扩展统一 i18n 到 Web Earth 动态入口、控制台/API 错误和公开页面,修复 Earth 通知胶囊、语言 switch、品牌栏、legend、tooltip、新闻/TV 英文态裁切与中文残留,并加入 harness 回归覆盖 | | `0.73.0` | feature | `dev` | `pending` | 新增前端统一 i18n、控制台语言/主题偏好入口、英文态 legacy 过渡翻译和 admin 一屏/状态指示器布局验证 | | `0.72.0` | feature | `dev` | `pending` | 新增完整 agent harness、单一 AGENTS 入口、Earth News smoke 覆盖和 collector 结构化日志清理,并同步控制台/Earth/Docs 响应式维护文档 | diff --git a/frontend/package.json b/frontend/package.json index 5a0b1281..4cb8a158 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "planet-frontend", - "version": "0.74.0", + "version": "0.74.1", "private": true, "packageManager": "bun@1", "dependencies": { diff --git a/frontend/src/admin/pages/PlainResourcePages.tsx b/frontend/src/admin/pages/PlainResourcePages.tsx index bcaa4341..7836a821 100644 --- a/frontend/src/admin/pages/PlainResourcePages.tsx +++ b/frontend/src/admin/pages/PlainResourcePages.tsx @@ -27,7 +27,7 @@ import { Trash2, X, } from 'lucide-react' -import { useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type PointerEvent as ReactPointerEvent, type ReactNode } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type CSSProperties, type DragEvent as ReactDragEvent, type PointerEvent as ReactPointerEvent, type ReactNode } from 'react' import { Link, useLocation, useNavigate } from 'react-router-dom' import MarkdownRenderer from '../../components/MarkdownRenderer/MarkdownRenderer' import Scrollbar from '../../components/Scrollbar/Scrollbar' @@ -132,6 +132,8 @@ type DatasourceMetricBaseline = { count: number } +type BrandAssetTargetKey = 'logo_src' | 'title_src' + const DEFAULT_DATASOURCE_FILTERS: DatasourceFilters = { product: '', module: '', @@ -871,6 +873,14 @@ type FieldConfig = { placeholder?: string disabled?: boolean help?: string + renderInput?: (props: { + disabled?: boolean + displayValue: unknown + fieldKey: string + onChange: (value: unknown) => void + placeholder?: string + value: unknown + }) => ReactNode inputAction?: { ariaLabel?: string disabled?: boolean @@ -2301,6 +2311,9 @@ function FieldGrid({ const className = field.wide ? 'an-field an-field--wide' : 'an-field' const searchTarget = searchGroupKey ? `${searchGroupKey}:field:${field.key}` : `field:${field.key}` const searchText = [label, field.key, text(displayValue, '')].filter(Boolean).join(' ') + const commitFieldValue = (nextValue: unknown) => { + onDraftChange(setNestedDraftField(draft, record, field.key, nextValue)) + } if (field.type === 'boolean') { return ( ) } + if (field.renderInput) { + return ( +
+ {label} + {field.renderInput({ + disabled: field.disabled, + displayValue, + fieldKey: field.key, + onChange: commitFieldValue, + placeholder, + value, + })} + {help ? {help} : null} +
+ ) + } const inputAction = field.inputAction ? { ...field.inputAction, @@ -2421,12 +2450,7 @@ function FieldGrid({ value={text(displayValue, '')} placeholder={placeholder} disabled={field.disabled} - onChange={(event) => onDraftChange(setNestedDraftField( - draft, - record, - field.key, - field.type === 'number' ? Number(event.target.value) : event.target.value, - ))} + onChange={(event) => commitFieldValue(field.type === 'number' ? Number(event.target.value) : event.target.value)} /> ) : ( @@ -2436,12 +2460,7 @@ function FieldGrid({ value={text(displayValue, '')} placeholder={placeholder} disabled={field.disabled} - onChange={(event) => onDraftChange(setNestedDraftField( - draft, - record, - field.key, - field.type === 'number' ? Number(event.target.value) : event.target.value, - ))} + onChange={(event) => commitFieldValue(field.type === 'number' ? Number(event.target.value) : event.target.value)} /> )} {help ? {help} : null} @@ -2545,6 +2564,25 @@ function GroupList({ const PLAYGROUND_SESSION_KEY = 'default' const BRAND_ASSET_ACCEPT = '.png,.jpg,.jpeg,.webp,.svg' const BRAND_ASSET_SUFFIXES = ['png', 'jpg', 'jpeg', 'webp', 'svg'] +const BRAND_ASSET_TARGET_META: Record = { + logo_src: { + copyLabel: '复制为 Logo', + dropLabel: '将图片拖到这里', + uploadLabel: '上传', + uploadTitle: '上传 Logo', + }, + title_src: { + copyLabel: '复制为标题图', + dropLabel: '将图片拖到这里', + uploadLabel: '上传', + uploadTitle: '上传标题图片', + }, +} const PLAYGROUND_PRESETS = [ { key: 'bgp-brief', @@ -2572,6 +2610,121 @@ const PLAYGROUND_PRESETS = [ }, ] +function isBrandAssetTargetKey(value: string): value is BrandAssetTargetKey { + return value === 'logo_src' || value === 'title_src' +} + +function isFileDrag(event: ReactDragEvent) { + return Array.from(event.dataTransfer.types).includes('Files') +} + +function BrandAssetInput({ + disabled, + onChange, + onFile, + placeholder, + target, + uploading, + value, +}: { + disabled?: boolean + onChange: (value: string) => void + onFile: (file: File) => void + placeholder?: string + target: BrandAssetTargetKey + uploading: boolean + value: string +}) { + const { locale } = useLocale() + const [dragging, setDragging] = useState(false) + const dragCounter = useRef(0) + const fileInputRef = useRef(null) + const meta = BRAND_ASSET_TARGET_META[target] + const suffixHelp = BRAND_ASSET_SUFFIXES.join(' / ') + + const resetDrag = () => { + dragCounter.current = 0 + setDragging(false) + } + + const handleDragEnter = (event: ReactDragEvent) => { + if (!isFileDrag(event)) return + event.preventDefault() + dragCounter.current += 1 + setDragging(true) + } + + const handleDragOver = (event: ReactDragEvent) => { + if (!isFileDrag(event)) return + event.preventDefault() + } + + const handleDragLeave = (event: ReactDragEvent) => { + if (!isFileDrag(event)) return + event.preventDefault() + dragCounter.current = Math.max(0, dragCounter.current - 1) + if (dragCounter.current === 0) setDragging(false) + } + + const handleDrop = (event: ReactDragEvent) => { + event.preventDefault() + resetDrag() + const file = event.dataTransfer.files?.[0] + if (file) onFile(file) + } + + const handleFileChange = (event: ChangeEvent) => { + const file = event.target.files?.[0] + event.target.value = '' + if (file) onFile(file) + } + + return ( +
+ onChange(event.target.value)} + placeholder={placeholder} + type="text" + value={value} + /> + + + {dragging ? ( + + ) : null} +
+ ) +} + function PlaygroundLite() { const { toast } = useToast() const { locale } = useLocale() @@ -2848,7 +3001,7 @@ function ModuleConsole({ config }: { config: ModuleConfig }) { const [revealedSecrets, setRevealedSecrets] = useState>({}) const [visibleSecretFields, setVisibleSecretFields] = useState>({}) const [smtpTestEmail, setSmtpTestEmail] = useState('') - const [brandUploadFile, setBrandUploadFile] = useState(null) + const [brandAssetUploadingTarget, setBrandAssetUploadingTarget] = useState(null) const [newsImportFile, setNewsImportFile] = useState(null) const newsImportInputRef = useRef(null) const [resolveTarget, setResolveTarget] = useState(null) @@ -4002,51 +4155,46 @@ function ModuleConsole({ config }: { config: ModuleConfig }) { setResolveTarget(null) } - const uploadBrandAsset = async () => { - if (!brandUploadFile) { - toast({ title: '请选择上传文件', tone: 'error' }) - return - } - const suffix = brandUploadFile.name.split('.').pop()?.toLowerCase() || '' - if (!BRAND_ASSET_SUFFIXES.includes(suffix)) { - toast({ title: '文件类型不支持', description: `仅支持 ${BRAND_ASSET_SUFFIXES.join(', ')}。`, tone: 'error' }) - return - } - const formData = new FormData() - formData.append('file', brandUploadFile) - setActionLoading(true) - try { - const response = await axios.post(apiPath('/earth/brand/assets'), formData, { - headers: { 'Content-Type': 'multipart/form-data' }, - }) - replaceSelectedWithPayload('brand-upload', '品牌资产上传', [{ - ...response.data, - __title: '品牌资产上传结果', - __module: '品牌资产', - __status: '已上传', - __metric: brandUploadFile.name, - }]) - setBrandUploadFile(null) - toast({ title: '品牌资产已上传', tone: 'success' }) - await load() - } catch (error) { - toast({ title: '品牌资产上传失败', description: error instanceof Error ? error.message : '接口请求失败', tone: 'error' }) - } finally { - setActionLoading(false) - } - } - - const selectBrandUploadFile = (file: File | null | undefined) => { + const uploadBrandAssetFile = async (file: File | null | undefined, target: BrandAssetTargetKey) => { if (!file) { - setBrandUploadFile(null) - return + toast({ title: '请拖入图片文件', tone: 'error' }) + return null } const suffix = file.name.split('.').pop()?.toLowerCase() || '' if (!BRAND_ASSET_SUFFIXES.includes(suffix)) { toast({ title: '文件类型不支持', description: `仅支持 ${BRAND_ASSET_SUFFIXES.join(', ')}。`, tone: 'error' }) - return + return null + } + const formData = new FormData() + formData.append('file', file) + setActionLoading(true) + setBrandAssetUploadingTarget(target) + try { + const response = await axios.post(apiPath('/earth/brand/assets'), formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }) + const assetUrl = text(response.data?.url, '') + if (!assetUrl) throw new Error('上传接口未返回资产 URL') + replaceSelectedWithPayload('brand-upload', '品牌资产上传', [{ + ...response.data, + __title: '品牌资产上传结果', + __module: target === 'logo_src' ? 'Logo' : '标题图片', + __status: '已上传', + __metric: file.name, + }]) + toast({ + title: target === 'logo_src' ? 'Logo 图片已上传' : '标题图片已上传', + description: assetUrl, + tone: 'success', + }) + return assetUrl + } catch (error) { + toast({ title: '品牌资产上传失败', description: actionErrorMessage(error), tone: 'error' }) + return null + } finally { + setActionLoading(false) + setBrandAssetUploadingTarget(null) } - setBrandUploadFile(file) } const createManualNewsGroup = async () => { @@ -5413,6 +5561,32 @@ function ModuleConsole({ config }: { config: ModuleConfig }) { { key: 'location_label', label: '位置标签' }, ] const fields = newsSourceFields.length ? newsSourceFields : newsItemFields.length ? newsItemFields : [...scalarFields, ...objectFields] + const uploadBrandAssetToDraft = async (file: File, target: BrandAssetTargetKey) => { + if (!activeGroup) return + const assetUrl = await uploadBrandAssetFile(file, target) + if (!assetUrl) return + setHierarchyDraft((current) => setNestedDraftField(current, activeGroup.record, target, assetUrl)) + } + const editableFields = config === configs.earthContent && activeSection.key === 'brand' + ? fields.map((field): FieldConfig => { + if (!isBrandAssetTargetKey(field.key)) return field + const target = field.key + return { + ...field, + renderInput: ({ disabled, displayValue, onChange, placeholder }) => ( + onChange(nextValue)} + onFile={(file) => void uploadBrandAssetToDraft(file, target)} + placeholder={placeholder} + target={target} + uploading={brandAssetUploadingTarget === target} + value={text(displayValue, '')} + /> + ), + } + }) + : fields const renderNewsFeedEditor = () => { if (!activeGroup) return null const currentSource = draftRecord(hierarchyDraft, activeGroup.record) @@ -5951,20 +6125,6 @@ function ModuleConsole({ config }: { config: ModuleConfig }) {
{config === configs.earthContent && activeSection.key === 'brand' ? ( <> - - ,