Uncheck a card to hide it from the dashboard. Every source stays available — nothing
is ever removed. Drag to reorder and resize cards from the dashboard’s
“Edit layout” mode.
);
}
// Refresh interval as a friendly value + unit (minutes / hours). The store still
// holds raw refreshSeconds; we convert on load and on save. Floors at 15 min / 1 hr.
type RefreshUnit = "min" | "hr";
function RefreshControl({
seconds,
onChange,
}: {
seconds: number;
onChange: (seconds: number) => void;
}) {
// A whole number of hours shows as hours; everything else as minutes.
const derivedUnit: RefreshUnit = seconds % 3600 === 0 && seconds >= 3600 ? "hr" : "min";
const [unit, setUnit] = useState(derivedUnit);
const floor = (u: RefreshUnit) => (u === "hr" ? 1 : 15);
// Display the true stored value (never lie about it); only the floor below
// applies when the user actually edits/commits a new interval.
const toValue = (s: number, u: RefreshUnit) =>
Math.max(1, Math.round(u === "hr" ? s / 3600 : s / 60));
const toSeconds = (v: number, u: RefreshUnit) =>
Math.max(floor(u), Math.round(v || 0)) * (u === "hr" ? 3600 : 60);
const [draft, setDraft] = useState(String(toValue(seconds, unit)));
// Re-sync the draft whenever the stored value or unit changes externally.
useEffect(() => setDraft(String(toValue(seconds, unit))), [seconds, unit]);
function changeUnit(u: RefreshUnit) {
setUnit(u); // keep the same real duration, re-expressed in the new unit
onChange(toSeconds(toValue(seconds, u), u));
}
return (
);
}
function SourceConfig({
state,
onPatch,
}: {
state: SourceState;
onPatch: (patch: Record) => void;
}) {
const cfg = state.source.config as Record;
if (state.source.kind === "news" || state.source.kind === "feeds") {
const feeds = Array.isArray(cfg.feeds) ? (cfg.feeds as string[]).join("\n") : "";
return (
);
}
if (state.source.kind === "obsidian") {
return (