Center live activity timer layout
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user