Files
willmiao-ComfyUI-Lora-Manager/static/js/utils/formatters.js
T
Will Miao 965bfd33cd feat(controls): keep model counts readable in large libraries
Counts in the filter panel chips (base models, tags, model types), the All Types
dropdown and the Refresh scope rows were rendered as raw digits, so a library with
10k+ models turned every chip into a five digit pill that dominated the row it sat
in — and the Refresh menu grouped its numbers while the filter panel did not.

One formatter now renders all of them (`formatCount`): exact and locale-grouped
below 10,000, abbreviated to three significant digits above it, and abbreviated
only where the locale's Intl compact form is a real unit and narrower than the
plain number — German keeps 18.475 instead of the rounded 18.500, Russian keeps
18 475 instead of the wider 18,5 тыс.

Their hover text is always present and always exact ({name} — {count} models, and
... recipes on the Recipes page): an affordance that only exists above a threshold
is undiscoverable, and the tooltip is also what reveals a chip label that CSS has
ellipsized.

Chips gain a two-slot layout (the name ellipsizes, the count keeps a tabular
right-aligned slot) so a long tag name can no longer push the count out of the
chip; Refresh rows move from float:right to a flex row, which keeps the counts in
a right-hand column and lets a long root label ellipsize. The All Types group
tooltip moves onto its trigger so it stops leaking onto the menu options.

The two new strings ship as [TODO: Translate] placeholders in the nine locales;
the translation pass is the next commit.
2026-10-18 18:34:16 +08:00

102 lines
3.8 KiB
JavaScript

import { i18n } from '../i18n/index.js';
import { translate } from './i18nHelpers.js';
/**
* Magnitude from which counts are abbreviated in dense UI (filter chips,
* dropdown rows). Below it the count is shown exact, so ordinary libraries
* look exactly like before; above it a five digit number would otherwise
* become the widest and loudest element of the row it sits in. The exact value
* stays available through `formatCountExact` for tooltips and aria-labels.
*/
const COUNT_COMPACT_FROM = 10000;
/**
* Format an entry count (models, recipes, tags) for dense UI.
*
* Abbreviated to ~3 significant digits above `compactFrom`, so a five digit
* count does not become the widest element of the row it sits in. The
* abbreviation is left to Intl, which follows the UI language ("19.9K",
* "1.99万", "19,9 k").
*
* Locales without a compact unit for the magnitude are detected and fall back
* to the exact, grouped count: German has no unit below a million, so its
* "compact" form is a *rounded* plain number ("18.500" for 18,475) that reads
* as exact but is not, and Russian's "19,9 тыс." is wider than "18 475". An
* abbreviation is only used when the locale actually produced one and it is
* shorter than the exact count.
*
* @param {number} value - The count
* @param {Object} [options]
* @param {number} [options.compactFrom] - Magnitude to start abbreviating at
* @returns {string} Formatted count (e.g. "42", "5,210", "19.9K")
*/
export function formatCount(value, { compactFrom = COUNT_COMPACT_FROM } = {}) {
const count = Number(value) || 0;
const exact = i18n.formatNumber(count);
if (count < compactFrom) {
return exact;
}
const compact = i18n.formatNumber(count, {
notation: 'compact',
maximumSignificantDigits: 3,
});
const roundedPlain = i18n.formatNumber(Number(count.toPrecision(3)));
return compact !== roundedPlain && compact.length < exact.length ? compact : exact;
}
/**
* Format an entry count in full, for tooltips and aria-labels that must not
* lose precision to `formatCount`'s abbreviation.
*
* @param {number} value - The count
* @returns {string} Exact, locale-grouped count (e.g. "19,876")
*/
export function formatCountExact(value) {
return i18n.formatNumber(Number(value) || 0);
}
/**
* Hover text for a counted chip or dropdown row: the full name plus the exact
* count.
*
* It is set unconditionally rather than only when the rendered count was
* abbreviated, for two reasons: the name is what CSS ellipsizes when a chip is
* narrow, so the tooltip is the only place the full label can be read; and an
* affordance that appears at some magnitudes and not others is unpredictable.
*
* @param {string} name - Label the count belongs to
* @param {number} value - The count
* @param {string} [key] - Translation key for the sentence
* @returns {string} e.g. "character — 18,475 models"
*/
export function formatCountTitle(name, value, key = 'header.filter.countTitle') {
const exact = formatCountExact(value);
return translate(key, { name, count: exact }, `${name} — ${exact} models`);
}
/**
* Format a file size in bytes to a human-readable string
* @param {number} bytes - The size in bytes
* @returns {string} Formatted size string (e.g., "1.5 MB")
*/
export function formatFileSize(bytes) {
if (!bytes || isNaN(bytes)) return '';
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
}
/**
* Convert timestamp to human readable date string
* @param {number} modified - Timestamp in seconds
* @returns {string} Formatted date string
*/
export function formatDate(modified) {
if (!modified) return '';
const date = new Date(modified * 1000);
return date.toLocaleString();
}