mirror of
https://github.com/gethomepage/homepage.git
synced 2025-12-07 09:35:54 -08:00
Refactor
This commit is contained in:
@@ -152,7 +152,7 @@ Widgets can tint their metric blocks automatically based on rules defined alongs
|
||||
value: "All good"
|
||||
```
|
||||
|
||||
Supported numeric operators for the `when` property are `gt`, `gte`, `lt`, `lte`, `eq`, `ne`, `between`, and `outside`. String rules support `equals`, `includes`, `startsWith`, `endsWith`, and `regex`. Each rule can be inverted with `negate: true`, and string rules may pass `caseSensitive: true` or custom regex `flags`. If you format values before passing them into `<Block>`, also pass the unformatted number or string via the `valueRaw` prop so the highlight engine can evaluate correctly.
|
||||
Supported numeric operators for the `when` property are `gt`, `gte`, `lt`, `lte`, `eq`, `ne`, `between`, and `outside`. String rules support `equals`, `includes`, `startsWith`, `endsWith`, and `regex`. Each rule can be inverted with `negate: true`, and string rules may pass `caseSensitive: true` or custom regex `flags`. The highlight engine does its best to coerce formatted values, but you will get the most reliable results when you pass plain numbers or strings into `<Block>`.
|
||||
|
||||
## Descriptions
|
||||
|
||||
|
||||
@@ -111,14 +111,14 @@ Supported colors are: `slate`, `gray`, `zinc`, `neutral`, `stone`, `amber`, `yel
|
||||
|
||||
## Block Highlight Levels
|
||||
|
||||
You can override the default Tailwind classes applied when a widget highlight rule resolves to the `good`, `warn`, or `danger` level. Only the `levels` map is read from `settings.yaml`; all highlight rules themselves are defined per widget in `services.yaml`.
|
||||
You can override the default Tailwind classes applied when a widget highlight rule resolves to the `good`, `warn`, or `danger` level.
|
||||
|
||||
```yaml
|
||||
blockHighlights:
|
||||
levels:
|
||||
good: "bg-emerald-400/30 text-emerald-900 dark:bg-emerald-900/30 dark:text-emerald-100"
|
||||
warn: "bg-amber-300/30 text-amber-900 dark:bg-amber-900/30 dark:text-amber-100"
|
||||
danger: "bg-rose-400/30 text-rose-900 dark:bg-rose-900/30 dark:text-rose-100"
|
||||
good: "bg-emerald-500/40 text-emerald-950 dark:bg-emerald-900/60 dark:text-emerald-400",
|
||||
warn: "bg-amber-300/30 text-amber-900 dark:bg-amber-900/30 dark:text-amber-200",
|
||||
danger: "bg-rose-700/45 text-rose-200 dark:bg-rose-950/70 dark:text-rose-400",
|
||||
```
|
||||
|
||||
Any unspecified level falls back to the built-in defaults.
|
||||
|
||||
@@ -6,7 +6,7 @@ import { evaluateHighlight, getHighlightClass } from "utils/highlights";
|
||||
|
||||
import { BlockHighlightContext } from "./highlight-context";
|
||||
|
||||
export default function Block({ value, valueRaw, label, field }) {
|
||||
export default function Block({ value, label, field }) {
|
||||
const { t } = useTranslation();
|
||||
const highlightConfig = useContext(BlockHighlightContext);
|
||||
|
||||
@@ -14,9 +14,8 @@ export default function Block({ value, valueRaw, label, field }) {
|
||||
if (!highlightConfig) return null;
|
||||
const fieldKey = field || label;
|
||||
if (!fieldKey) return null;
|
||||
const candidateValue = valueRaw ?? value;
|
||||
return evaluateHighlight(fieldKey, candidateValue, highlightConfig);
|
||||
}, [field, label, value, valueRaw, highlightConfig]);
|
||||
return evaluateHighlight(fieldKey, value, highlightConfig);
|
||||
}, [field, label, value, highlightConfig]);
|
||||
|
||||
const highlightClass = useMemo(() => {
|
||||
if (!highlight?.level) return undefined;
|
||||
|
||||
@@ -13,8 +13,14 @@ const normalizeFieldKeys = (fields, widgetType) => {
|
||||
const trimmedKey = key.trim();
|
||||
if (trimmedKey === "") return acc;
|
||||
|
||||
const targetKey = widgetType ? `${widgetType}.${trimmedKey}` : trimmedKey;
|
||||
acc[targetKey] = value;
|
||||
acc[trimmedKey] = value;
|
||||
|
||||
if (widgetType && !trimmedKey.includes(".")) {
|
||||
const namespacedKey = `${widgetType}.${trimmedKey}`;
|
||||
if (!(namespacedKey in acc)) {
|
||||
acc[namespacedKey] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
@@ -76,65 +82,59 @@ const parseNumericValue = (value) => {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return undefined;
|
||||
|
||||
const numericMatch = trimmed.match(/[-+]?\d+(?:[\d.,]*\d)?/);
|
||||
if (!numericMatch) return undefined;
|
||||
const direct = Number(trimmed);
|
||||
if (!Number.isNaN(direct)) return direct;
|
||||
|
||||
const numeric = numericMatch[0];
|
||||
const separators = numeric.match(/[.,]/g) ?? [];
|
||||
const uniqueSeparators = [...new Set(separators)];
|
||||
const compact = trimmed.replace(/\s+/g, "");
|
||||
if (!compact || !/^[-+]?[0-9.,]+$/.test(compact)) return undefined;
|
||||
|
||||
if (uniqueSeparators.length === 0) {
|
||||
const parsed = Number(numeric);
|
||||
return Number.isNaN(parsed) ? undefined : parsed;
|
||||
}
|
||||
const commaCount = (compact.match(/,/g) || []).length;
|
||||
const dotCount = (compact.match(/\./g) || []).length;
|
||||
|
||||
if (uniqueSeparators.length >= 2) {
|
||||
const lastComma = numeric.lastIndexOf(",");
|
||||
const lastDot = numeric.lastIndexOf(".");
|
||||
const decimalSeparator = lastComma > lastDot ? "," : ".";
|
||||
const thousandsSeparator = decimalSeparator === "." ? "," : ".";
|
||||
|
||||
let canonical = numeric;
|
||||
const thousandsPattern = thousandsSeparator === "." ? /\./g : /,/g;
|
||||
canonical = canonical.replace(thousandsPattern, "");
|
||||
if (decimalSeparator === ",") {
|
||||
canonical = canonical.replace(/,/g, ".");
|
||||
}
|
||||
|
||||
const parsed = Number(canonical);
|
||||
return Number.isNaN(parsed) ? undefined : parsed;
|
||||
}
|
||||
|
||||
const separator = uniqueSeparators[0];
|
||||
const occurrences = separators.length;
|
||||
|
||||
if (separator === ".") {
|
||||
if (occurrences === 1) {
|
||||
const parsed = Number(numeric);
|
||||
if (commaCount && dotCount) {
|
||||
const lastComma = compact.lastIndexOf(",");
|
||||
const lastDot = compact.lastIndexOf(".");
|
||||
if (lastComma > lastDot) {
|
||||
const asDecimal = compact.replace(/\./g, "").replace(/,/g, ".");
|
||||
const parsed = Number(asDecimal);
|
||||
return Number.isNaN(parsed) ? undefined : parsed;
|
||||
}
|
||||
|
||||
const canonical = numeric.replace(/\./g, "");
|
||||
const parsed = Number(canonical);
|
||||
const asThousands = compact.replace(/,/g, "");
|
||||
const parsed = Number(asThousands);
|
||||
return Number.isNaN(parsed) ? undefined : parsed;
|
||||
}
|
||||
|
||||
if (separator === ",") {
|
||||
if (occurrences === 1) {
|
||||
const decimalCandidate = Number(numeric.replace(/,/g, "."));
|
||||
const thousandsCandidate = Number(numeric.replace(/,/g, ""));
|
||||
|
||||
const candidates = [thousandsCandidate, decimalCandidate].filter((candidate) => !Number.isNaN(candidate));
|
||||
if (candidates.length === 0) return undefined;
|
||||
if (candidates.length === 1) return candidates[0];
|
||||
const uniqueCandidates = [...new Set(candidates)];
|
||||
return uniqueCandidates.length === 1 ? uniqueCandidates[0] : uniqueCandidates;
|
||||
if (commaCount) {
|
||||
const parts = compact.split(",");
|
||||
if (commaCount === 1 && parts[1]?.length <= 2) {
|
||||
const parsed = Number(compact.replace(",", "."));
|
||||
if (!Number.isNaN(parsed)) return parsed;
|
||||
}
|
||||
const isGrouped = parts.length > 1 && parts.slice(1).every((part) => part.length === 3);
|
||||
if (isGrouped) {
|
||||
const parsed = Number(compact.replace(/,/g, ""));
|
||||
if (!Number.isNaN(parsed)) return parsed;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const canonical = numeric.replace(/,/g, "");
|
||||
const parsed = Number(canonical);
|
||||
if (dotCount) {
|
||||
const parts = compact.split(".");
|
||||
if (dotCount === 1 && parts[1]?.length <= 2) {
|
||||
const parsed = Number(compact);
|
||||
if (!Number.isNaN(parsed)) return parsed;
|
||||
}
|
||||
const isGrouped = parts.length > 1 && parts.slice(1).every((part) => part.length === 3);
|
||||
if (isGrouped) {
|
||||
const parsed = Number(compact.replace(/\./g, ""));
|
||||
if (!Number.isNaN(parsed)) return parsed;
|
||||
}
|
||||
const parsed = Number(compact);
|
||||
return Number.isNaN(parsed) ? undefined : parsed;
|
||||
}
|
||||
|
||||
const parsed = Number(compact);
|
||||
return Number.isNaN(parsed) ? undefined : parsed;
|
||||
}
|
||||
|
||||
if (typeof value === "object" && value !== null && "props" in value) {
|
||||
|
||||
Reference in New Issue
Block a user