Center live activity timer layout

This commit is contained in:
2026-07-01 02:37:58 -04:00
parent b7eef743bd
commit 107b593002
+70 -88
View File
@@ -1,7 +1,9 @@
import { HStack, Image, Spacer, Text } from "@expo/ui/swift-ui";
import { HStack, Image, Text, VStack } from "@expo/ui/swift-ui";
import {
frame,
font,
foregroundStyle,
layoutPriority,
lineLimit,
minimumScaleFactor,
monospacedDigit,
@@ -12,120 +14,100 @@ import { createLiveActivity, type LiveActivityEnvironment } from "expo-widgets";
import type { TimeClockActivityProps } from "@/lib/time-clock-live-activity.types";
const TIMER_HORIZON_MS = 24 * 60 * 60 * 1000;
function liveTimer(
startedAtMs: number,
modifiers: ReturnType<typeof font>[],
) {
const lower = new Date(startedAtMs);
const upper = new Date(startedAtMs + TIMER_HORIZON_MS);
return (
<Text
timerInterval={{ lower, upper }}
countsDown={false}
modifiers={modifiers}
/>
);
}
function TimeClockActivity(props: TimeClockActivityProps, _environment: LiveActivityEnvironment) {
"widget";
const green = "green";
const title = props.description.trim() || "Clock In";
const clientLabel = props.clientName.trim() || title;
const subtitle = props.invoiceLabel.trim();
const startedAtMs = props.startedAtMs > 0 ? props.startedAtMs : Date.now();
const island = "#FFFFFF";
const businessLabel = props.businessName.trim();
const clientLabel = props.clientName.trim();
const title = clientLabel || businessLabel || "Clock In";
const timerMods = [
font({ design: "monospaced", weight: "bold", size: 20 }),
monospacedDigit(),
foregroundStyle(green),
font({ weight: "bold", size: 17 }),
foregroundStyle({ type: "hierarchical", style: "primary" }),
lineLimit(1),
minimumScaleFactor(0.85),
minimumScaleFactor(0.75),
layoutPriority(2),
frame({ width: 100, alignment: "center" }),
];
const compactTimerMods = [
font({ design: "monospaced", weight: "semibold", size: 11 }),
monospacedDigit(),
foregroundStyle(green),
foregroundStyle(island),
lineLimit(1),
minimumScaleFactor(0.8),
minimumScaleFactor(0.75),
frame({ minWidth: 38, alignment: "center" }),
layoutPriority(2),
];
const clientMods = [
font({ weight: "semibold", size: 13 }),
font({ weight: "bold", size: 17 }),
foregroundStyle({ type: "hierarchical", style: "primary" }),
lineLimit(1),
minimumScaleFactor(0.85),
minimumScaleFactor(0.75),
];
const subtitleMods = [
font({ size: 11 }),
foregroundStyle({ type: "hierarchical", style: "secondary" }),
lineLimit(1),
minimumScaleFactor(0.85),
// Avoid greedy layout primitives here: the live timerInterval Text can
// collapse when paired with Spacer/maxWidth. Fixed centered boxes keep the
// content stable without forcing left/right edge alignment.
const titleRowMods = [
layoutPriority(1),
frame({ width: 140, alignment: "center" }),
];
const sideZoneMods = [
frame({ width: 100, alignment: "center" }),
];
const bannerRowMods = [
padding({ horizontal: 10, vertical: 10 }),
frame({ maxWidth: Infinity, alignment: "center" }),
];
const bannerTimer = liveTimer(startedAtMs, timerMods);
const compactTimer = liveTimer(startedAtMs, compactTimerMods);
const bannerTimer = <Text modifiers={timerMods}>{props.elapsedShort}</Text>;
const compactTimer = <Text modifiers={compactTimerMods}>{props.elapsedShort}</Text>;
const logoLarge = (
<Image
assetName="SplashScreenLogo"
systemName="dollarsign.circle.fill"
size={22}
modifiers={[
foregroundStyle({ type: "hierarchical", style: "primary" }),
frame({ width: 22, height: 22 }),
widgetAccentedRenderingMode("fullColor"),
]}
/>
);
const logoSmall = (
<Image
systemName="dollarsign.circle.fill"
color={island}
size={14}
modifiers={[frame({ width: 14, height: 14 }), widgetAccentedRenderingMode("fullColor")]}
/>
);
return {
banner: (
<HStack alignment="center" spacing={8} modifiers={[padding({ horizontal: 14, vertical: 12 })]}>
<Image
systemName="dollarsign.circle.fill"
color={green}
size={22}
modifiers={[widgetAccentedRenderingMode("fullColor")]}
/>
<Text modifiers={clientMods}>{clientLabel}</Text>
<Spacer minLength={12} />
<HStack alignment="center" spacing={8} modifiers={bannerRowMods}>
<VStack alignment="center" modifiers={sideZoneMods}>
{logoLarge}
</VStack>
<VStack alignment="center" spacing={1} modifiers={titleRowMods}>
<Text modifiers={clientMods}>{title}</Text>
</VStack>
{bannerTimer}
</HStack>
),
bannerSmall: (
<HStack alignment="center" spacing={8} modifiers={[padding({ horizontal: 12, vertical: 10 })]}>
<Image
systemName="dollarsign.circle.fill"
color={green}
size={18}
modifiers={[widgetAccentedRenderingMode("fullColor")]}
/>
<Text modifiers={clientMods}>{clientLabel}</Text>
<Spacer minLength={8} />
{compactTimer}
<HStack alignment="center" spacing={8} modifiers={bannerRowMods}>
<VStack alignment="center" modifiers={sideZoneMods}>
{logoLarge}
</VStack>
<VStack alignment="center" spacing={1} modifiers={titleRowMods}>
<Text modifiers={clientMods}>{title}</Text>
</VStack>
{bannerTimer}
</HStack>
),
compactLeading: (
<Image
systemName="dollarsign.circle.fill"
color={green}
size={15}
modifiers={[widgetAccentedRenderingMode("fullColor")]}
/>
),
compactLeading: logoSmall,
compactTrailing: compactTimer,
minimal: (
<Image
systemName="dollarsign.circle.fill"
color={green}
size={12}
modifiers={[widgetAccentedRenderingMode("fullColor")]}
/>
),
expandedLeading: (
<Image
systemName="dollarsign.circle.fill"
color={green}
size={20}
modifiers={[widgetAccentedRenderingMode("fullColor")]}
/>
),
expandedCenter: <Text modifiers={clientMods}>{clientLabel}</Text>,
expandedTrailing: bannerTimer,
expandedBottom: (
<Text modifiers={subtitleMods}>{subtitle || "beenvoice"}</Text>
),
minimal: logoSmall,
};
}