Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
567e1bd
docs: add initial screen redesign exploration baseline
ScottMorris Mar 8, 2026
0b221a2
docs: add redesign journey log with speaker scripts
ScottMorris Mar 8, 2026
d5faa62
docs: align redesign plan with desktop constraints and scaffold UI specs
ScottMorris Mar 8, 2026
a0319c7
docs: model mobile and desktop concepts explicitly in UI specs
ScottMorris Mar 8, 2026
4f2c6bc
docs: record policy to keep redesign churn tracked in git
ScottMorris Mar 8, 2026
d4a72a3
docs: add proposed-v2 redesign SVG set and update review plan links
ScottMorris Mar 8, 2026
a458cd6
docs: add proposed-v3 mockups with refined desktop and settings direc…
ScottMorris Mar 8, 2026
804ae50
docs: add code-faithful mobile Settings mockup (v4) and settle key de…
ScottMorris Mar 8, 2026
2f8c8c1
docs: close open question on Next alarm banner data source
ScottMorris Mar 8, 2026
ad994ca
docs: regenerate full v4 mockup set with consistent structure and dec…
ScottMorris Mar 8, 2026
26c217f
docs: correct desktop mockups and add OS window context view (v4)
ScottMorris Mar 8, 2026
6ef4716
docs: add Window alarm mode mockups for Edit screen (v4)
ScottMorris Mar 8, 2026
a8b7b6e
docs: add phase-by-phase UI redesign implementation plan
ScottMorris Mar 8, 2026
07d1980
docs: add implementation plan and settle all open design decisions
ScottMorris Mar 8, 2026
69e8bc9
docs: log desktop window size decision (760 × 680)
ScottMorris Mar 8, 2026
7d026f4
docs: reorder and number all journey log entries, add missing entries
ScottMorris Mar 8, 2026
2dfa31a
docs: add implementing agent prompt
ScottMorris Mar 8, 2026
f13d298
docs: restructure redesign artefacts into permanent home
ScottMorris Mar 8, 2026
00458e9
feat(ui): add foundation tokens and update desktop window size
ScottMorris Mar 8, 2026
5c66dc3
feat(ui): redesign home screen with accent rails, banner, and pull-to…
ScottMorris Mar 8, 2026
8d557a6
fix(ui): theme edit screen controls and fix DaySelector dark mode border
ScottMorris Mar 8, 2026
24348bf
feat(ui): add desktop Settings nav rail with section panels
ScottMorris Mar 8, 2026
e6c8b52
style(ui): desktop spacing and footer polish
ScottMorris Mar 8, 2026
8c876f5
fix(a11y): add reduced motion support to SwipeToDeleteRow
ScottMorris Mar 8, 2026
7ae0ebd
docs: add implementation log entries for phases 5 and 6
ScottMorris Mar 8, 2026
9da4040
fix: reset drag flag after swipe snap-back so taps register as clicks
ScottMorris Mar 8, 2026
2b65053
fix(window): lock size to 760×680, persist position only
ScottMorris Mar 8, 2026
e04bdc1
feat(titlebar): drive minimize/maximize buttons from Tauri window cap…
ScottMorris Mar 8, 2026
8b01fad
fix(window): remove redundant unmaximize call on startup
ScottMorris Mar 8, 2026
bc5f6d7
fix(window): persist position across sessions; hide maximize on non-r…
ScottMorris Mar 8, 2026
c2c8a64
fix(ui): edit screen layout — side-by-side window pickers, tighter sp…
ScottMorris Mar 8, 2026
e8a5a45
fix(ui): constrain edit form to 700px and fix scroll behind footer
ScottMorris Mar 8, 2026
06f0418
fix(ui): enable edit screen scrolling and scale window pickers to fit
ScottMorris Mar 8, 2026
98c9fbb
fix(ui): fix scroll container and replace scale with CSS overrides
ScottMorris Mar 8, 2026
1badfad
feat(ui): home screen banner gradient, single-line format, and footer…
ScottMorris Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions apps/threshold/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@
"fs:allow-write-text-file",
"core:window:allow-show",
"core:window:allow-set-focus",
"core:window:allow-maximize",
"core:window:allow-unmaximize",
"core:window:allow-toggle-maximize",
"core:window:allow-minimize",
"core:window:allow-unminimize",
"core:window:allow-close",
Expand Down
3 changes: 1 addition & 2 deletions apps/threshold/src-tauri/capabilities/desktop.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
"permissions": [
"window-state:default",
"core:webview:allow-create-webview-window",
"core:window:allow-destroy",
"core:window:allow-set-size"
"core:window:allow-destroy"
]
}
6 changes: 5 additions & 1 deletion apps/threshold/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@ pub fn run() {

#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
builder = builder.plugin(tauri_plugin_window_state::Builder::new().build());
builder = builder.plugin(
tauri_plugin_window_state::Builder::new()
.with_state_flags(tauri_plugin_window_state::StateFlags::POSITION)
.build(),
);
}

builder = builder.invoke_handler(tauri::generate_handler![
Expand Down
6 changes: 3 additions & 3 deletions apps/threshold/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
"windows": [
{
"title": "Threshold",
"width": 450,
"height": 800,
"resizable": true,
"width": 760,
"height": 680,
"resizable": false,
"maximizable": false,
"decorations": false,
"visible": true
Expand Down
7 changes: 5 additions & 2 deletions apps/threshold/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,12 @@ const App: React.FC = () => {
try {
try {
await win.setDecorations(false); // Force removal of native title bar
await win.unmaximize();
// await win.setSize(new LogicalSize(450, 800));
await win.center();
// Only centre on first launch (no saved position yet)
const pos = await win.outerPosition();
if (pos.x === 0 && pos.y === 0) {
await win.center();
}
} catch (e) {
console.error('Failed to resize/decorate window:', e);
}
Expand Down
16 changes: 10 additions & 6 deletions apps/threshold/src/components/AlarmItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { format } from 'date-fns';
import { TimeFormatHelper } from '../utils/TimeFormatHelper';
import { PlatformUtils } from '../utils/PlatformUtils';
import { SwipeToDeleteRow } from './SwipeToDeleteRow';
import { accentRailSx } from '../theme/alarmCardStyles';
import { UI } from '../theme/uiTokens';

interface AlarmItemProps {
alarm: AlarmRecord;
Expand Down Expand Up @@ -49,21 +51,23 @@ export const AlarmItem: React.FC<AlarmItemProps> = ({
onClick={!isMobile ? onClick : undefined}
sx={{
width: '100%',
// Mobile "bubble" styling is handled by the wrapper now
mb: isMobile ? 0 : undefined,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
p: 2,
pl: 1.5,
cursor: 'pointer',
borderRadius: isMobile ? '16px' : undefined, // Bubble look on mobile, default on desktop
// Ionic items are usually list items.
// Let's keep card look but maybe reduced elevation or spacing on mobile
boxShadow: isMobile ? 'none' : undefined, // Remove shadow inside the swipe row
position: 'relative',
overflow: 'hidden',
borderRadius: isMobile ? UI.card.borderRadius : undefined,
boxShadow: isMobile ? 'none' : undefined,
bgcolor: 'background.paper',
borderBottom: isMobile ? 'none' : undefined, // Remove list separator look
borderBottom: isMobile ? 'none' : undefined,
}}
>
{/* Accent rail */}
<Box sx={accentRailSx(alarm.enabled)} />
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="h5" component="div" sx={{ fontWeight: 'bold' }}>
{timeDisplay}
Expand Down
3 changes: 2 additions & 1 deletion apps/threshold/src/components/DaySelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export const DaySelector: React.FC<DaySelectorProps> = ({ selectedDays, onChange
key={index}
aria-label={label}
sx={{
border: '1px solid rgba(0, 0, 0, 0.12)',
border: '1px solid',
borderColor: 'divider',
'&.Mui-selected': {
backgroundColor: 'primary.main',
color: 'primary.contrastText',
Expand Down
60 changes: 60 additions & 0 deletions apps/threshold/src/components/NextAlarmBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add required licence header to new TypeScript files

The repo guideline in /workspace/threshold/AGENTS.md ("Licence and Copyright") requires each source file to begin with the summary/copyright/SPDX header, but this new file starts directly with imports. That introduces a compliance regression in this commit (and the same pattern appears in other newly added TS files), so the required header block should be added at the top.

Useful? React with 👍 / 👎.

import { Box, Typography } from '@mui/material';
import { alpha, useTheme } from '@mui/material/styles';
import { AccessTime as AccessTimeIcon } from '@mui/icons-material';
import { AlarmRecord } from '../types/alarm';
import { TimeFormatHelper } from '../utils/TimeFormatHelper';
import { UI } from '../theme/uiTokens';

interface NextAlarmBannerProps {
alarms: AlarmRecord[];
is24h: boolean;
}

export const NextAlarmBanner: React.FC<NextAlarmBannerProps> = ({ alarms, is24h }) => {
const theme = useTheme();

const now = Date.now();
const nextAlarm = alarms
Comment on lines +17 to +18
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh next-alarm countdown while Home stays open

The countdown is derived from Date.now() during render but there is no interval/ticking state, so the banner text becomes stale if the user leaves Home open (for example, after 10 minutes it still shows the original "in Xh Ym" value until another state change rerenders). Since this UI is presented as a live countdown, it should be recomputed periodically.

Useful? React with 👍 / 👎.

.filter((a) => a.enabled && a.nextTrigger && a.nextTrigger > now)
.sort((a, b) => a.nextTrigger! - b.nextTrigger!)
[0];

if (!nextAlarm) return null;

const triggerDate = new Date(nextAlarm.nextTrigger!);
const diffMs = nextAlarm.nextTrigger! - now;
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));

const countdownParts: string[] = [];
if (diffHours > 0) countdownParts.push(`${diffHours}h`);
countdownParts.push(`${diffMinutes}m`);
const countdown = countdownParts.join(' ');

const formattedTime = TimeFormatHelper.format(triggerDate, is24h);

const ariaLabel = `Next alarm in ${countdown}, at ${formattedTime}`;

return (
<Box
role="status"
aria-label={ariaLabel}
sx={{
background: `linear-gradient(to right, ${alpha(theme.palette.primary.main, 0.16)}, ${alpha(theme.palette.primary.main, 0.03)})`,
borderRadius: UI.banner.borderRadius,
px: 2,
py: 1.5,
mb: 2,
display: 'flex',
alignItems: 'center',
gap: 1.5,
}}
>
<AccessTimeIcon sx={{ color: 'primary.main', fontSize: 20 }} />
<Typography variant="body2" sx={{ fontWeight: 600, color: 'primary.main' }}>
Next alarm in {countdown} · Scheduled: {formattedTime}
</Typography>
</Box>
);
};
111 changes: 111 additions & 0 deletions apps/threshold/src/components/PullToRefresh.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React, { useRef, useState, useCallback } from 'react';
import { Box, CircularProgress } from '@mui/material';
import { motion, useMotionValue, useTransform, animate } from 'motion/react';

const PULL_THRESHOLD = 72;

interface PullToRefreshProps {
onRefresh: () => void;
children: React.ReactNode;
}

export const PullToRefresh: React.FC<PullToRefreshProps> = ({ onRefresh, children }) => {
const containerRef = useRef<HTMLDivElement>(null);
const [isRefreshing, setIsRefreshing] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const pullY = useMotionValue(0);
const spinnerOpacity = useTransform(pullY, [0, PULL_THRESHOLD], [0, 1]);
const spinnerScale = useTransform(pullY, [0, PULL_THRESHOLD], [0.5, 1]);

const prefersReducedMotion =
typeof window !== 'undefined' &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches;

const startYRef = useRef<number | null>(null);

const handlePointerDown = useCallback((e: React.PointerEvent) => {
const el = containerRef.current;
if (!el || el.scrollTop > 0 || isRefreshing) return;
startYRef.current = e.clientY;
setIsDragging(true);
}, [isRefreshing]);

const handlePointerMove = useCallback((e: React.PointerEvent) => {
if (!isDragging || startYRef.current === null) return;
const el = containerRef.current;
if (!el || el.scrollTop > 0) {
setIsDragging(false);
pullY.set(0);
startYRef.current = null;
return;
}
const delta = Math.max(0, e.clientY - startYRef.current);
// Dampen the pull for a natural feel
const dampened = Math.min(delta * 0.5, PULL_THRESHOLD * 1.5);
pullY.set(dampened);
}, [isDragging, pullY]);

const handlePointerUp = useCallback(() => {
if (!isDragging) return;
setIsDragging(false);
startYRef.current = null;

const currentPull = pullY.get();
if (currentPull >= PULL_THRESHOLD) {
setIsRefreshing(true);
onRefresh();
// Reset after a short delay to show the spinner
setTimeout(() => {
setIsRefreshing(false);
if (prefersReducedMotion) {
pullY.set(0);
} else {
animate(pullY, 0, { type: 'spring', stiffness: 300, damping: 30 });
}
}, 600);
} else {
if (prefersReducedMotion) {
pullY.set(0);
} else {
animate(pullY, 0, { type: 'spring', stiffness: 300, damping: 30 });
}
}
}, [isDragging, pullY, onRefresh, prefersReducedMotion]);

return (
<Box
ref={containerRef}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onPointerCancel={handlePointerUp}
sx={{
position: 'relative',
touchAction: 'pan-x',
overflowY: 'auto',
Comment on lines +84 to +85
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use vertical pan touch-action for pull-to-refresh list

On mobile Home, this wrapper is the scroll container (overflowY: 'auto') but touchAction: 'pan-x' disables native vertical panning, so once the alarm list is taller than the viewport users cannot scroll down to reach lower alarms. This makes part of the primary list inaccessible in a common scenario (many alarms). Use a touch-action that permits vertical scrolling and keep the pull-to-refresh threshold logic in JS.

Useful? React with 👍 / 👎.

flexGrow: 1,
}}
>
{/* Pull indicator */}
<motion.div
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
overflow: 'hidden',
height: pullY,
opacity: spinnerOpacity,
scale: spinnerScale,
}}
>
<CircularProgress
size={28}
color="primary"
variant={isRefreshing ? 'indeterminate' : 'determinate'}
value={isRefreshing ? undefined : 100}
/>
</motion.div>
{children}
</Box>
);
};
30 changes: 21 additions & 9 deletions apps/threshold/src/components/SwipeToDeleteRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export const SwipeToDeleteRow: React.FC<SwipeToDeleteRowProps> = ({
const x = useMotionValue(0);
const controls = useAnimation();
const [isDeleting, setIsDeleting] = useState(false);
const prefersReducedMotion =
typeof window !== 'undefined' &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches;

const containerRef = useRef<HTMLDivElement>(null);

Expand Down Expand Up @@ -49,17 +52,26 @@ export const SwipeToDeleteRow: React.FC<SwipeToDeleteRowProps> = ({

if ((isPastThreshold || isFastFling) && !isDeleting) {
setIsDeleting(true);
// Animate off screen in the direction of the swipe
const direction = offset > 0 ? 1 : -1;
await controls.start({
x: direction * width * 1.5,
transition: { duration: 0.2 }
});
// Trigger delete callback
onDelete();
if (prefersReducedMotion) {
onDelete();
} else {
// Animate off screen in the direction of the swipe
const direction = offset > 0 ? 1 : -1;
await controls.start({
x: direction * width * 1.5,
transition: { duration: 0.2 }
});
onDelete();
}
} else {
// Spring back to start
controls.start({ x: 0, transition: { type: 'spring', stiffness: 400, damping: 25 } });
if (prefersReducedMotion) {
await controls.start({ x: 0, transition: { duration: 0 } });
} else {
await controls.start({ x: 0, transition: { type: 'spring', stiffness: 400, damping: 25 } });
}
// Reset drag flag so subsequent taps register as clicks
isDrag.current = false;
}
};

Expand Down
Loading
Loading