Add 'apps/mobile/' from commit '5fa30f365f21531094cd4d2045042bb4f1370ac3'
git-subtree-dir: apps/mobile git-subtree-mainline:86f8987dffgit-subtree-split:5fa30f365f
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
import { HStack, Image, Text, VStack } from "@expo/ui/swift-ui";
|
||||
import {
|
||||
frame,
|
||||
font,
|
||||
foregroundStyle,
|
||||
layoutPriority,
|
||||
lineLimit,
|
||||
minimumScaleFactor,
|
||||
monospacedDigit,
|
||||
padding,
|
||||
truncationMode,
|
||||
widgetAccentedRenderingMode,
|
||||
} from "@expo/ui/swift-ui/modifiers";
|
||||
import { createLiveActivity, type LiveActivityEnvironment } from "expo-widgets";
|
||||
|
||||
import type { TimeClockActivityProps } from "@/lib/time-clock-live-activity.types";
|
||||
|
||||
function TimeClockActivity(props: TimeClockActivityProps, _environment: LiveActivityEnvironment) {
|
||||
"widget";
|
||||
|
||||
const island = "#FFFFFF";
|
||||
const businessLabel = props.businessName.trim();
|
||||
const clientLabel = props.clientName.trim();
|
||||
const title = clientLabel || businessLabel || "Clock In";
|
||||
|
||||
const timerMods = [
|
||||
font({ weight: "bold", size: 17 }),
|
||||
foregroundStyle({ type: "hierarchical", style: "primary" }),
|
||||
lineLimit(1),
|
||||
minimumScaleFactor(0.75),
|
||||
layoutPriority(2),
|
||||
frame({ width: 100, alignment: "center" }),
|
||||
];
|
||||
// A live timer Text reserves a large ideal width (for the H:MM:SS format at
|
||||
// the Dynamic Island's font), so with no width cap it inflates the compact
|
||||
// pill to full width. Pin it to a snug fixed width sized for the common
|
||||
// MM:SS case; minimumScaleFactor lets the occasional H:MM:SS scale down to
|
||||
// fit instead of widening the pill. This keeps the pill tight with no gap.
|
||||
const compactTimerMods = [
|
||||
font({ design: "monospaced", weight: "semibold", size: 11 }),
|
||||
monospacedDigit(),
|
||||
foregroundStyle(island),
|
||||
lineLimit(1),
|
||||
minimumScaleFactor(0.6),
|
||||
frame({ width: 40, alignment: "center" }),
|
||||
];
|
||||
const clientMods = [
|
||||
font({ weight: "bold", size: 17 }),
|
||||
foregroundStyle({ type: "hierarchical", style: "primary" }),
|
||||
lineLimit(1),
|
||||
truncationMode("tail"),
|
||||
];
|
||||
// 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 startedAt = new Date(props.startedAtMs);
|
||||
// Bounded to 24h so SwiftUI reserves width for the H:MM:SS format only.
|
||||
// (An open-ended `.timer`/`.date` style reserves width for a huge duration,
|
||||
// which blows the compact Dynamic Island pill out to full width.)
|
||||
const timerRange = {
|
||||
lower: startedAt,
|
||||
upper: new Date(props.startedAtMs + 24 * 60 * 60 * 1000),
|
||||
};
|
||||
|
||||
// Banner (Notification Center / Lock Screen): timerInterval inside a fixed
|
||||
// 100pt box, which absorbs the reserved width.
|
||||
const bannerTimer = (
|
||||
<Text modifiers={timerMods} timerInterval={timerRange} countsDown={false} />
|
||||
);
|
||||
// Dynamic Island (compact): same bounded timerInterval, sized intrinsically —
|
||||
// no fixed/minWidth frame or layoutPriority, so the pill hugs the reserved
|
||||
// H:MM:SS width instead of being padded out or stretched.
|
||||
const compactTimer = (
|
||||
<Text modifiers={compactTimerMods} timerInterval={timerRange} countsDown={false} />
|
||||
);
|
||||
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")]}
|
||||
/>
|
||||
);
|
||||
|
||||
// Apple Watch / CarPlay (`bannerSmall`). The strip is only ~150-180pt wide,
|
||||
// so it cannot use the iPhone banner's fixed 100/140/100 columns (those
|
||||
// overflow and push the timer off-screen, leaving just its first digit).
|
||||
// Instead: fixed-size logo, a flexible client name that truncates to fill
|
||||
// the leftover space, and a compact fixed-width timer that stays fully
|
||||
// visible (fixed width — not maxWidth — so the live timer text doesn't
|
||||
// collapse).
|
||||
const watchRowMods = [
|
||||
padding({ horizontal: 10, vertical: 6 }),
|
||||
frame({ maxWidth: Infinity, alignment: "center" }),
|
||||
];
|
||||
const watchTitleMods = [
|
||||
font({ weight: "semibold", size: 14 }),
|
||||
foregroundStyle({ type: "hierarchical", style: "primary" }),
|
||||
lineLimit(1),
|
||||
truncationMode("tail"),
|
||||
frame({ maxWidth: Infinity, alignment: "leading" }),
|
||||
];
|
||||
const watchTimerMods = [
|
||||
font({ design: "monospaced", weight: "bold", size: 14 }),
|
||||
monospacedDigit(),
|
||||
foregroundStyle({ type: "hierarchical", style: "primary" }),
|
||||
lineLimit(1),
|
||||
minimumScaleFactor(0.6),
|
||||
frame({ width: 58, alignment: "trailing" }),
|
||||
];
|
||||
const watchLogo = (
|
||||
<Image
|
||||
assetName="SplashScreenLogo"
|
||||
systemName="dollarsign.circle.fill"
|
||||
size={16}
|
||||
modifiers={[
|
||||
foregroundStyle({ type: "hierarchical", style: "primary" }),
|
||||
frame({ width: 16, height: 16 }),
|
||||
widgetAccentedRenderingMode("fullColor"),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
const watchTimer = (
|
||||
<Text modifiers={watchTimerMods} timerInterval={timerRange} countsDown={false} />
|
||||
);
|
||||
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}
|
||||
</HStack>
|
||||
),
|
||||
bannerSmall: (
|
||||
<HStack alignment="center" spacing={6} modifiers={watchRowMods}>
|
||||
{watchLogo}
|
||||
<Text modifiers={watchTitleMods}>{title}</Text>
|
||||
{watchTimer}
|
||||
</HStack>
|
||||
),
|
||||
compactLeading: logoSmall,
|
||||
compactTrailing: compactTimer,
|
||||
minimal: logoSmall,
|
||||
};
|
||||
}
|
||||
|
||||
export default createLiveActivity("TimeClockActivity", TimeClockActivity);
|
||||
Reference in New Issue
Block a user