mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
fix(ui): give the segmented control back the chart tabs' look
The shared control introduced a muted track with the chosen segment raised on the background colour. In a chart header it read heavier than the card around it, larger type and wider segments on a grey bar, and it dropped the primary fill the range tabs had carried. Segmented now draws the chart tabs' former look everywhere: no track, the chosen segment filled in the primary colour, the others plain until hovered. A small size keeps the range rows in chart headers at the former type and padding; the touch floor of 44 px on a coarse pointer stays.
This commit is contained in:
1 parent
c1fb59fe39
commit
f9796858e6
9 files changed
+48
-7
No files matched your search
@@ -51,6 +51,7 @@ export function ChartRangeTabs({
|
||||
const { t } = useTranslations();
|
||||
return (
|
||||
<Segmented
|
||||
size="sm"
|
||||
mode="toggle"
|
||||
label={t("charts.rangeLabel")}
|
||||
optionSlot={() => "chart-range-tab"}
|
||||
|
||||
@@ -140,6 +140,7 @@ export function CustomMetricChart({
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-end">
|
||||
<Segmented
|
||||
size="sm"
|
||||
options={RANGE_DAYS.map((r) => ({
|
||||
value: r.key,
|
||||
label: t(`customMetrics.chart.range.${r.key}`),
|
||||
|
||||
@@ -141,6 +141,7 @@ export function MoodDimensionTrends({
|
||||
return (
|
||||
<div className="space-y-3" data-slot="mood-dimension-trends">
|
||||
<Segmented
|
||||
size="sm"
|
||||
options={MOOD_DIMENSION_WINDOWS.map((option) => ({
|
||||
value: String(option),
|
||||
label: t("insights.mood.dimensions.window", { days: option }),
|
||||
|
||||
@@ -177,6 +177,7 @@ export function PeriodNarrativeCard({
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
{/* Calm segmented control, no chart. */}
|
||||
<Segmented
|
||||
size="sm"
|
||||
mode="toggle"
|
||||
label={t("insights.narrativeTitle")}
|
||||
slot="period-narrative-period"
|
||||
|
||||
@@ -392,6 +392,7 @@ export function SleepStageStackedBar({ breakdown }: SleepStageStackedBarProps) {
|
||||
{/* The page's other range controls read "7T / 30T"; this one
|
||||
printed an English "7d" beside them, hence `daysNLabel`. */}
|
||||
<Segmented
|
||||
size="sm"
|
||||
mode="toggle"
|
||||
label={t("charts.rangeLabel")}
|
||||
slot="sleep-stage-window-toggle"
|
||||
|
||||
@@ -254,6 +254,7 @@ export function LabBiomarkerChart({
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-end">
|
||||
<Segmented
|
||||
size="sm"
|
||||
options={RANGE_DAYS.map((r) => ({
|
||||
value: r.key,
|
||||
label: t(`labs.chart.range.${r.key}`),
|
||||
|
||||
@@ -260,6 +260,7 @@ function TargetCard({
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-end">
|
||||
<Segmented
|
||||
size="sm"
|
||||
mode="toggle"
|
||||
value={scaleMode}
|
||||
options={[
|
||||
|
||||
@@ -61,6 +61,27 @@ describe("<Segmented>", () => {
|
||||
expect(out).toContain("min-h-9");
|
||||
});
|
||||
|
||||
it("fills the chosen segment in the primary colour, on no track", () => {
|
||||
const out = html();
|
||||
const segments = out.match(/<button[^>]*>/g) ?? [];
|
||||
expect(segments).toHaveLength(2);
|
||||
const chosen = segments.filter((b) => b.includes('aria-checked="true"'));
|
||||
expect(chosen).toHaveLength(1);
|
||||
expect(chosen[0]).toContain("bg-primary text-primary-foreground");
|
||||
const other = segments.find((b) => b.includes('aria-checked="false"'));
|
||||
expect(other).not.toContain("bg-primary");
|
||||
const group = out.match(/<div[^>]*role="radiogroup"[^>]*>/)?.[0];
|
||||
expect(group).not.toContain("bg-muted");
|
||||
});
|
||||
|
||||
it("sets a chart header's range row in the small type", () => {
|
||||
expect(html()).toContain("text-sm");
|
||||
const small = html({ size: "sm" });
|
||||
expect(small).toContain("text-xs");
|
||||
expect(small).not.toContain("text-sm");
|
||||
expect(small.match(/pointer-coarse:min-h-11/g)?.length).toBe(2);
|
||||
});
|
||||
|
||||
it("renders pressed buttons in toggle mode", () => {
|
||||
const out = html({ mode: "toggle" });
|
||||
expect(out).toContain('role="group"');
|
||||
|
||||
@@ -3,8 +3,15 @@
|
||||
/**
|
||||
* The segmented control: a short row of mutually exclusive choices on one
|
||||
* track (zoom level, chronicle grouping, cards or table, recent or all,
|
||||
* Paperless-ngx or Papra). One look everywhere: a muted track, the chosen
|
||||
* segment raised on the background colour, never coloured.
|
||||
* Paperless-ngx or Papra). One look everywhere, the one the dashboard chart
|
||||
* tabs always had: no track, the chosen segment filled in the primary colour,
|
||||
* the others plain until hovered. A muted track with a raised segment was
|
||||
* tried and read as heavier than the card around it.
|
||||
*
|
||||
* Two sizes: `md` (default) for a choice that is the content of its area (a
|
||||
* view switch, a form sheet, a detail page's sections), `sm` for a range row
|
||||
* in a chart header, where the segments sit beside the chart's title and
|
||||
* must not outweigh it.
|
||||
*
|
||||
* Two semantics, one chrome:
|
||||
*
|
||||
@@ -59,6 +66,8 @@ export interface SegmentedProps<T extends string> {
|
||||
iconOnly?: boolean;
|
||||
/** Spread the segments over the full width. */
|
||||
stretch?: boolean;
|
||||
/** `sm` for a range row in a chart header; see the module comment. */
|
||||
size?: "sm" | "md";
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
/**
|
||||
@@ -78,6 +87,7 @@ export function Segmented<T extends string>({
|
||||
mode = "radio",
|
||||
iconOnly = false,
|
||||
stretch = false,
|
||||
size = "md",
|
||||
disabled = false,
|
||||
className,
|
||||
wrapOption,
|
||||
@@ -100,7 +110,7 @@ export function Segmented<T extends string>({
|
||||
aria-disabled={disabled || undefined}
|
||||
data-slot={slot}
|
||||
className={cn(
|
||||
"bg-muted inline-flex shrink-0 items-center gap-0.5 rounded-lg p-0.5",
|
||||
"inline-flex shrink-0 items-center gap-1",
|
||||
stretch && "flex w-full",
|
||||
className,
|
||||
)}
|
||||
@@ -135,14 +145,17 @@ export function Segmented<T extends string>({
|
||||
onChange(option.value);
|
||||
}}
|
||||
className={cn(
|
||||
"focus-visible:ring-ring/50 inline-flex items-center justify-center gap-1.5 rounded-md text-sm font-medium transition-colors outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none",
|
||||
"focus-visible:ring-ring/50 inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 motion-reduce:transition-none",
|
||||
size === "sm" ? "text-xs" : "text-sm",
|
||||
iconOnly
|
||||
? "size-9 pointer-coarse:size-11"
|
||||
: "min-h-9 px-3 pointer-coarse:min-h-11",
|
||||
: size === "sm"
|
||||
? "min-h-9 px-2 sm:px-3 pointer-coarse:min-h-11 pointer-coarse:min-w-11"
|
||||
: "min-h-9 px-3 pointer-coarse:min-h-11",
|
||||
stretch && "flex-1",
|
||||
checked
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-foreground/75 hover:text-foreground dark:text-tab-inactive dark:hover:text-foreground",
|
||||
? "bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
: "text-foreground hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
)}
|
||||
>
|
||||
{Icon ? (
|
||||
|
||||
Reference in new issue
Block a user