diff --git a/selfdrive/carrot/web/css/pages/settings/device.css b/selfdrive/carrot/web/css/pages/settings/device.css index a20a99e58c..f113d5be9b 100644 --- a/selfdrive/carrot/web/css/pages/settings/device.css +++ b/selfdrive/carrot/web/css/pages/settings/device.css @@ -1047,7 +1047,7 @@ } .page--setting #settingScreenItems .setting-popular-detail-block .setting-popular-detail__row { - grid-template-columns: 42px minmax(0, 1fr) 48px; + grid-template-columns: 18px minmax(0, 1fr) 48px; min-height: 48px; padding: 8px 11px; gap: 10px; @@ -1072,24 +1072,28 @@ border-bottom: 1px solid color-mix(in srgb, var(--md-outline-var) 16%, transparent); } -.setting-popular-detail__rank, .setting-popular-detail__count { color: var(--md-on-surface-var); font-size: var(--fs-label-sm); font-weight: 850; } -.page--setting #settingScreenItems .setting-popular-detail-block .setting-popular-detail__rank { +.setting-popular-detail__marker { display: inline-flex; align-items: center; justify-content: center; - width: 32px; - height: 28px; + width: 18px; + height: 18px; +} + +.setting-popular-detail__marker::before { + content: ""; + display: block; + width: 6px; + height: 6px; border-radius: 999px; - background: color-mix(in srgb, var(--md-primary) 12%, var(--md-surface-cont-h)); - color: color-mix(in srgb, var(--md-on-surface-var) 78%, transparent); - font-size: 10px; - font-weight: 900; + background: color-mix(in srgb, var(--md-primary) 46%, var(--md-on-surface-var)); + opacity: 0.72; } .setting-popular-detail__main { diff --git a/selfdrive/carrot/web/index.html b/selfdrive/carrot/web/index.html index e140662601..ed811bb590 100644 --- a/selfdrive/carrot/web/index.html +++ b/selfdrive/carrot/web/index.html @@ -99,7 +99,7 @@ - + @@ -705,9 +705,9 @@

Home

- - - + + + @@ -722,7 +722,7 @@

Home

- + diff --git a/selfdrive/carrot/web/js/pages/setting.js b/selfdrive/carrot/web/js/pages/setting.js index b5170ab403..0c38db1ecd 100644 --- a/selfdrive/carrot/web/js/pages/setting.js +++ b/selfdrive/carrot/web/js/pages/setting.js @@ -972,16 +972,39 @@ function isSettingPopularValueInRange(p, raw) { return value >= min && value <= max; } +function compareSettingPopularValueItems(p, a, b, aIndex = 0, bIndex = 0) { + const aCount = Number(a?.count ?? 0); + const bCount = Number(b?.count ?? 0); + const safeACount = Number.isFinite(aCount) ? aCount : 0; + const safeBCount = Number.isFinite(bCount) ? bCount : 0; + if (safeACount !== safeBCount) return safeBCount - safeACount; + + const aValue = normalizeSettingPopularNumericValue(p, a?.value); + const bValue = normalizeSettingPopularNumericValue(p, b?.value); + if (aValue !== null && bValue !== null && aValue !== bValue) return aValue - bValue; + if (aValue !== null && bValue === null) return -1; + if (aValue === null && bValue !== null) return 1; + + const aText = formatSettingPopularValue(p, a?.value); + const bText = formatSettingPopularValue(p, b?.value); + const textOrder = aText.localeCompare(bText, LANG === "ko" ? "ko-KR" : undefined, { numeric: true, sensitivity: "base" }); + if (textOrder !== 0) return textOrder; + + return aIndex - bIndex; +} + function getSettingPopularDisplayEntry(p, entry) { if (!entry || typeof entry !== "object") return null; const sample = Number(entry?.sample ?? entry?.sample_count ?? 0); if (!Number.isFinite(sample) || sample < 1) return null; if (!isSettingPopularValueInRange(p, entry?.value)) return null; const topValues = Array.isArray(entry?.top_values) - ? entry.top_values.filter((item) => { + ? entry.top_values.map((item, index) => ({ item, index })).filter(({ item }) => { const count = Number(item?.count ?? 0); return Number.isFinite(count) && count > 0 && isSettingPopularValueInRange(p, item?.value); - }).slice(0, 10) + }).sort((a, b) => compareSettingPopularValueItems(p, a.item, b.item, a.index, b.index)) + .slice(0, 10) + .map(({ item }) => item) : []; return { ...entry, top_values: topValues }; } @@ -990,25 +1013,65 @@ function getSettingPopularCarKeyLabel() { return String(settingPopularValuesState.carKey || "").trim() || getUIText("setting_popular_value_my_model", "내 차종"); } +function getSettingPopularPrimaryCount(entry) { + const values = Array.isArray(entry?.top_values) ? entry.top_values : []; + const count = Number(values[0]?.count ?? entry?.top_count ?? entry?.count ?? 0); + return Number.isFinite(count) ? count : 0; +} + +function getSettingPopularSummaryValues(entry) { + const values = Array.isArray(entry?.top_values) ? entry.top_values : []; + if (!values.length) return []; + + const topCount = getSettingPopularPrimaryCount(entry); + if (topCount < 2) return []; + + const tiedValues = values.filter((item) => { + const count = Number(item?.count ?? 0); + return Number.isFinite(count) && count === topCount; + }); + + return tiedValues.length <= 2 ? tiedValues : []; +} + +function hasSettingPopularClearTop(entry) { + return getSettingPopularSummaryValues(entry).length > 0; +} + function renderSettingPopularChipText(p, entry) { - const sample = Number(entry?.sample ?? entry?.sample_count ?? 0); - const value = formatSettingPopularValue(p, entry?.value); - if (!sample || !value) return ""; - return getUIText("setting_popular_value_chip", "{label} ({sample}대) {value}", { - label: getUIText("setting_popular_value_chip_label", "내 차종 인기값"), + const summaryValues = getSettingPopularSummaryValues(entry); + if (!summaryValues.length) return ""; + const sample = getSettingPopularPrimaryCount(entry); + const values = summaryValues.map((item) => formatSettingPopularValue(p, item?.value)).filter(Boolean); + if (!sample || !values.length) return ""; + const label = getUIText("setting_popular_value_chip_label", "내 차종 인기값"); + if (values.length === 1) { + return getUIText("setting_popular_value_chip", "{label} ({sample}대) {value}", { + label, + sample, + value: values[0], + }); + } + return getUIText("setting_popular_value_chip_tied", "{label} (각 {sample}대) {values}", { + label, sample, - value: `"${value}"`, + values: values.join(" · "), }); } function renderSettingPopularChipHtml(p, entry) { - const sample = Number(entry?.sample ?? entry?.sample_count ?? 0); - const value = formatSettingPopularValue(p, entry?.value); - if (!sample || !value) return ""; + const summaryValues = getSettingPopularSummaryValues(entry); + if (!summaryValues.length) return ""; + const sample = getSettingPopularPrimaryCount(entry); + const values = summaryValues.map((item) => formatSettingPopularValue(p, item?.value)).filter(Boolean); + if (!sample || !values.length) return ""; + const sampleText = values.length === 1 + ? getUIText("setting_popular_value_chip_sample", "{sample}대", { sample }) + : getUIText("setting_popular_value_chip_each_sample", "각 {sample}대", { sample }); return ` ${escapeHtml(getUIText("setting_popular_value_chip_label", "내 차종 인기값"))} - (${escapeHtml(getUIText("setting_popular_value_chip_sample", "{sample}대", { sample }))}) - ${escapeHtml(`"${value}"`)} + (${escapeHtml(sampleText)}) + ${escapeHtml(values.join(" · "))} `; } @@ -1040,14 +1103,13 @@ function renderSettingPopularDetailHtml(p, entry) { const counts = values.map((item) => Number(item?.count ?? 0)).filter((count) => Number.isFinite(count) && count > 0); const maxCount = Math.max(1, ...counts); - const rows = values.map((item, index) => { - const rank = Number(item?.rank) || index + 1; + const rows = values.map((item) => { const value = formatSettingPopularValue(p, item?.value); const count = Number(item?.count ?? 0); const width = Math.max(4, Math.min(100, Math.round((Math.max(0, count) / maxCount) * 100))); return `