Center live activity timer layout

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