Skip to content

Polish settings hierarchy and interaction states - #9

Merged
emmsixx merged 5 commits into
codex/shortcut-recorderfrom
codex/settings-visual-polish
Aug 12, 2026
Merged

emmsixx merged 5 commits into
codex/shortcut-recorderfrom
codex/settings-visual-polish

Conversation

@emmsixx

@emmsixx emmsixx commented Aug 12, 2026

Copy link
Copy Markdown
Owner

What changed

  • clarify settings copy so each field explains the outcome in plain language
  • simplify the settings surface treatment and remove the nested custom-colors card noise
  • replace the generic rocket with a shortcut/behavior cue and use a tighter accent system
  • use the native system UI font stack and add reduced-motion coverage

Review note

This is intentionally stacked on PR #8 so the visual-only diff stays focused. Merge/rebase PR #8 first, then retarget this PR to main if desired.

Verification

  • ./node_modules/.bin/tsc --noEmit
  • ./node_modules/.bin/vite build
  • git diff --check
  • npx --yes impeccable detect src --json → no findings

Summary by CodeRabbit

  • New Features

    • Added a redesigned Always On Clock settings experience with sections for clock, appearance, behavior, and about.
    • Added live clock previews, theme selection, custom colors, transparency controls, font sizing, and date/time formatting options.
    • Added global shortcut recording, launch-on-startup controls, reset settings, and repository/license links.
    • Added improved keyboard navigation and accessibility support across controls.
  • Bug Fixes

    • Improved save status handling and prevented failed updates from overwriting newer settings.
    • Updated application branding and window presentation.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a83f4a83-c7b5-44a7-b401-deae5d3bf465

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR redesigns the Always On Clock display and settings window. It adds shared clock formatting, persisted save status, reusable settings controls, a development preview harness, scoped link permissions, new styling, and product documentation.

Changes

Always On Clock application

Layer / File(s) Summary
Clock display and formatting
src/utils/clockFormat.ts, src/hooks/useClockDisplay.ts, src/components/ClockFace.tsx, src/components/ClockPreview.tsx, src/Clock.tsx, src/App.tsx, src/globals.css
The clock now uses shared formatting utilities, a timer hook, and reusable rendering for time, date, colors, opacity, and scaling.
Settings state and window flow
src/contexts/SettingsContext.tsx, src/SettingsWindow.tsx, src/TitleBar.tsx, src-tauri/capabilities/settings.json, vite.config.ts, src/vite-env.d.ts
Settings saves now report status and handle stale failures. The settings window gains section navigation, reset handling, responsive sizing, and version display support.
Settings controls and panes
src/components/ui/*, src/components/panes/*, src/components/ThemePicker.tsx, src/components/ShortcutRecorder.tsx, src/globals.css
The legacy settings UI is replaced with reusable controls and dedicated Clock, Appearance, Behavior, and About panes.
Preview and application metadata
preview.html, src/dev-preview.tsx, index.html, pnpm-workspace.yaml, PRODUCT.md, src/globals.css
The PR adds browser preview wiring, Tauri stubs, Always On Clock branding, esbuild build permission, and product documentation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SettingsWindow
  participant SettingsPane
  participant SettingsContext
  participant SettingsStore
  SettingsWindow->>SettingsPane: Render selected settings section
  SettingsPane->>SettingsContext: Submit setting update
  SettingsContext->>SettingsStore: Persist serialized update
  SettingsStore-->>SettingsContext: Return save result
  SettingsContext-->>SettingsWindow: Show save timestamp or error
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the PR, which involves refactoring the settings interface structure and improving interaction states across multiple components.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@emmsixx
emmsixx force-pushed the codex/settings-visual-polish branch 2 times, most recently from 2766a72 to 8696f10 Compare August 12, 2026 14:01
@emmsixx
emmsixx force-pushed the codex/settings-visual-polish branch from 9a1e197 to 5db65bf Compare August 12, 2026 14:08
- Add sectioned Clock, Appearance, Behavior, and About panes
- Share clock rendering between overlay and settings preview
- Refresh window chrome, styling, controls, and preview tooling
@emmsixx

emmsixx commented Aug 12, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/App.tsx (1)

41-57: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

The shared debounce timer drops one of the two saves.

handleMove and handleResize share saveTimeout. If a resize follows a move inside 500 ms, the resize clears the pending position save, so the new position is never persisted. The reverse order loses the new size. Window drag-resize emits both events, so this happens in normal use.

Use one timer per concern.

🐛 Proposed fix
     const win = getCurrentWindow();
-    let saveTimeout: number | null = null;
+    let moveTimeout: number | null = null;
+    let resizeTimeout: number | null = null;
 
     const handleMove = async () => {
-      if (saveTimeout) clearTimeout(saveTimeout);
-      saveTimeout = window.setTimeout(async () => {
+      if (moveTimeout) clearTimeout(moveTimeout);
+      moveTimeout = window.setTimeout(async () => {
         const pos = await win.innerPosition();
         void updateSettings({ windowPosition: { x: pos.x, y: pos.y } }).catch(() => undefined);
       }, 500);
     };
 
     const handleResize = async () => {
-      if (saveTimeout) clearTimeout(saveTimeout);
-      saveTimeout = window.setTimeout(async () => {
+      if (resizeTimeout) clearTimeout(resizeTimeout);
+      resizeTimeout = window.setTimeout(async () => {
         const size = await win.innerSize();
         void updateSettings({
           windowSize: { width: size.width, height: size.height },
         }).catch(() => undefined);
       }, 500);
     };

Also clear both timers in the cleanup function:

     return () => {
-      if (saveTimeout) clearTimeout(saveTimeout);
+      if (moveTimeout) clearTimeout(moveTimeout);
+      if (resizeTimeout) clearTimeout(resizeTimeout);
       unlistenMove.then((fn) => fn());
       unlistenResize.then((fn) => fn());
     };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/App.tsx` around lines 41 - 57, Use separate debounce timers for the
position and size persistence in handleMove and handleResize, so each concern
can save independently without cancelling the other. Update the cleanup logic to
clear both timers, preserving the existing 500 ms delay and updateSettings
calls.
vite.config.ts (1)

3-14: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Enable JSON module resolution for the Vite config.

tsconfig.node.json lacks resolveJsonModule, so TypeScript 5.9 rejects the package.json import. Add resolveJsonModule: true and include package.json to avoid TS6307.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vite.config.ts` around lines 3 - 14, Update tsconfig.node.json to enable
resolveJsonModule and include package.json in its file or include configuration,
so the package import used by the Vite config’s pkg symbol type-checks
successfully.
🧹 Nitpick comments (6)
src/hooks/useClockDisplay.ts (1)

12-25: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Consider aligning the tick to the second boundary.

A fixed 1000 ms interval drifts against the wall clock. With seconds shown, the display can skip or repeat a second. Scheduling the next update at the next second boundary removes the drift. The hook also re-renders every second when showSeconds is false, where only the minute matters.

♻️ Optional refactor
   useEffect(() => {
-    const interval = window.setInterval(() => setNow(new Date()), 1000);
-    return () => window.clearInterval(interval);
-  }, []);
+    let timer = 0;
+    const schedule = () => {
+      const next = 1000 - (Date.now() % 1000);
+      timer = window.setTimeout(() => {
+        setNow(new Date());
+        schedule();
+      }, next);
+    };
+    schedule();
+    return () => window.clearTimeout(timer);
+  }, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/useClockDisplay.ts` around lines 12 - 25, Update the timer setup in
useClockDisplay to schedule each refresh at the next wall-clock second boundary
instead of using a fixed 1000 ms interval, rescheduling after each update to
prevent drift. When showSeconds is false, use minute-aligned scheduling so the
hook does not re-render every second; preserve the existing cleanup and
formatted time/date output.
src/TitleBar.tsx (1)

64-67: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Remove the unused event listener after the promise settles.

webview.once returns a cleanup function. The current code discards both cleanup functions, so the listener for the event that does not occur remains registered. Store both cleanup functions and call them in finally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/TitleBar.tsx` around lines 64 - 67, Update the promise setup in
TitleBar’s webview event handling to retain both cleanup functions returned by
the tauri://created and tauri://error webview.once calls, then invoke both
cleanup functions in a finally block so the unused listener is removed
regardless of which event settles the promise.
src/components/ThemePicker.tsx (1)

67-75: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hide the decorative sample from assistive technology.

The stage contains the literal text 12:00. A screen reader reads it as part of each radio name, so every option is announced as "12:00 ". Add aria-hidden="true" to theme-tile-stage; the tile name already identifies the option.

♻️ Proposed change
-            <span className="theme-tile-stage">
+            <span className="theme-tile-stage" aria-hidden="true">
               <span className="theme-tile-ramp" aria-hidden="true" />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ThemePicker.tsx` around lines 67 - 75, Add aria-hidden="true"
to the theme-tile-stage element in ThemePicker so the decorative 12:00 sample is
excluded from assistive technology while the existing theme name continues
identifying each option.
src/components/panes/ClockPane.tsx (1)

31-34: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Derive the time-format samples from the shared formatter.

'2:45 PM' and '14:45' are hardcoded. The overlay renders time through src/utils/clockFormat.ts, which applies the runtime locale. In a locale that renders 2:45 p.m. or uses a different separator, the sample does not match the clock the user sees. Build both samples with the shared formatter from a fixed reference date, as dateOptions already does for dates.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/panes/ClockPane.tsx` around lines 31 - 34, Replace the
hardcoded samples in the ClockPane options with values generated by the shared
formatter from a fixed reference date, matching the existing dateOptions
pattern. Reuse the formatter and its 12-hour/24-hour configuration so samples
follow the runtime locale and match the rendered clock.
src/components/panes/AppearancePane.tsx (1)

82-104: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Continuous inputs persist a write per event. Sliders and the native color picker emit an event for every intermediate value. Each event calls updateSettings, and each call appends a store write to the serialized save queue in src/contexts/SettingsContext.tsx. One drag can produce dozens of disk writes and can delay the lastSavedAt acknowledgement in src/SettingsWindow.tsx. Introduce one shared debounce boundary for continuous controls: apply the value to state immediately for the live preview, and persist the last value after the interaction settles.

  • src/components/panes/AppearancePane.tsx#L82-L104: debounce the backgroundOpacity and textOpacity writes instead of calling updateSettings on every slider step.
  • src/components/ui/ColorField.tsx#L48-L55: debounce the type="color" onChange write, or commit the color on blur.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/panes/AppearancePane.tsx` around lines 82 - 104, The
continuous appearance controls currently persist every intermediate value; add
one shared debounce boundary so live state updates immediately while only the
final settled value is persisted. In src/components/panes/AppearancePane.tsx
lines 82-104, debounce writes from the Background and Text sliders; in
src/components/ui/ColorField.tsx lines 48-55, debounce the type="color" onChange
persistence or commit it on blur. Ensure each control preserves immediate
preview updates while preventing one write per event.
src/SettingsWindow.tsx (1)

129-142: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Announce save failures assertively.

The live region uses aria-live="polite". A failed save waits for the screen reader to finish other output. Give the status element a role that matches the state: alert for saveError, status otherwise.

♻️ Proposed status role
           <p
             className={`settings-status ${saveError ? "is-error" : justSaved ? "is-saved" : ""}`.trim()}
+            role={saveError ? "alert" : "status"}
           >
@@
-            <span aria-live="polite">
+            <span>
               {saveError ? saveError : justSaved ? "Saved" : "Changes save instantly"}
             </span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/SettingsWindow.tsx` around lines 129 - 142, Update the live-region
element in the settings status block to use an assertive alert role when
saveError is present and a status role otherwise, while preserving the existing
save message and conditional styling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pnpm-workspace.yaml`:
- Around line 1-2: Pin the repository’s package manager to pnpm 10.26.0 or later
via the package.json packageManager field, and update the pnpm versions used by
both CI and release workflows to the same supported version. Keep the existing
allowBuilds configuration for esbuild unchanged.

In `@src/components/panes/AboutPane.tsx`:
- Around line 65-94: Update the confirmingReset toggle in AboutPane so focus
moves to the mounted Cancel button when confirmation opens and returns to the
Restore button when confirmation closes. Add stable refs and an effect or
equivalent focus management tied to the state transition, ensuring each target
is focused only after it is mounted while preserving the existing reset
behavior.

In `@src/components/ShortcutRecorder.tsx`:
- Around line 262-266: Update the reset button in ShortcutRecorder to reuse the
existing save-handling helper used by the record flow, including saveAttemptRef
invalidation and feedback/error handling, instead of calling
onChange(defaultValue) directly. Extract or reuse an applyShortcut-style
function and invoke it from both the reset handler and keydown path, with the
reset passing defaultValue so rejected saves are surfaced to feedback.

In `@src/components/ui/ColorField.tsx`:
- Around line 19-29: Update the value synchronization useEffect in ColorField so
it only calls setDraft when the incoming value differs from the current draft
after normalization, preserving the user’s original casing and caret during
uppercase hex entry. Keep handleTextChange’s lowercase onChange behavior
unchanged.

In `@src/components/ui/Field.tsx`:
- Around line 17-40: Update the Field component’s control rendering so the
generated hintId is passed to the associated control through aria-describedby.
Ensure both the control prop and children path can consume this association,
preserving the existing hintId generation and rendering behavior.

In `@src/contexts/SettingsContext.tsx`:
- Around line 226-261: Update updateSettings to roll back only the keys included
in updates when saveOperation fails, rather than restoring the entire
previousSettings object or requiring settingsRef.current === nextSettings.
Preserve unrelated settings changes applied by cross-window synchronization or
store onChange, while clearing the save error and restoring the failed keys from
their pre-update values only when this update remains the latest relevant
version.

In `@src/dev-preview.tsx`:
- Around line 26-40: Update the invoke handler in src/dev-preview.tsx so
unsupported browser commands no longer fall through to a silent undefined
result. Add explicit stubs for promised interactions such as the SettingsWindow
close command and an event-listening path that can emit events, and make the
default branch throw for any remaining unsupported command.

In `@src/globals.css`:
- Around line 10-47: Update the Stylelint value-keyword-case configuration to
ignore the case-sensitive keywords BlinkMacSystemFont and optimizeLegibility,
preserving their casing in the --font-ui and text-rendering declarations without
changing runtime behavior.

In `@src/utils/clockFormat.ts`:
- Around line 6-18: Update formatTime to derive an hour12 boolean from
timeFormat and reuse it for both the hour option and hour12 setting; use numeric
hours when hour12 is true and 2-digit hours otherwise, preserving the existing
minute and seconds formatting.

---

Outside diff comments:
In `@src/App.tsx`:
- Around line 41-57: Use separate debounce timers for the position and size
persistence in handleMove and handleResize, so each concern can save
independently without cancelling the other. Update the cleanup logic to clear
both timers, preserving the existing 500 ms delay and updateSettings calls.

In `@vite.config.ts`:
- Around line 3-14: Update tsconfig.node.json to enable resolveJsonModule and
include package.json in its file or include configuration, so the package import
used by the Vite config’s pkg symbol type-checks successfully.

---

Nitpick comments:
In `@src/components/panes/AppearancePane.tsx`:
- Around line 82-104: The continuous appearance controls currently persist every
intermediate value; add one shared debounce boundary so live state updates
immediately while only the final settled value is persisted. In
src/components/panes/AppearancePane.tsx lines 82-104, debounce writes from the
Background and Text sliders; in src/components/ui/ColorField.tsx lines 48-55,
debounce the type="color" onChange persistence or commit it on blur. Ensure each
control preserves immediate preview updates while preventing one write per
event.

In `@src/components/panes/ClockPane.tsx`:
- Around line 31-34: Replace the hardcoded samples in the ClockPane options with
values generated by the shared formatter from a fixed reference date, matching
the existing dateOptions pattern. Reuse the formatter and its 12-hour/24-hour
configuration so samples follow the runtime locale and match the rendered clock.

In `@src/components/ThemePicker.tsx`:
- Around line 67-75: Add aria-hidden="true" to the theme-tile-stage element in
ThemePicker so the decorative 12:00 sample is excluded from assistive technology
while the existing theme name continues identifying each option.

In `@src/hooks/useClockDisplay.ts`:
- Around line 12-25: Update the timer setup in useClockDisplay to schedule each
refresh at the next wall-clock second boundary instead of using a fixed 1000 ms
interval, rescheduling after each update to prevent drift. When showSeconds is
false, use minute-aligned scheduling so the hook does not re-render every
second; preserve the existing cleanup and formatted time/date output.

In `@src/SettingsWindow.tsx`:
- Around line 129-142: Update the live-region element in the settings status
block to use an assertive alert role when saveError is present and a status role
otherwise, while preserving the existing save message and conditional styling.

In `@src/TitleBar.tsx`:
- Around line 64-67: Update the promise setup in TitleBar’s webview event
handling to retain both cleanup functions returned by the tauri://created and
tauri://error webview.once calls, then invoke both cleanup functions in a
finally block so the unused listener is removed regardless of which event
settles the promise.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cb1f8369-6917-4c97-b83b-dff2cca0316f

📥 Commits

Reviewing files that changed from the base of the PR and between 0e70690 and e44c400.

📒 Files selected for processing (30)
  • PRODUCT.md
  • index.html
  • pnpm-workspace.yaml
  • preview.html
  • src-tauri/capabilities/settings.json
  • src/App.tsx
  • src/Clock.tsx
  • src/SettingsWindow.tsx
  • src/TitleBar.tsx
  • src/components/ClockFace.tsx
  • src/components/ClockPreview.tsx
  • src/components/Settings.tsx
  • src/components/ShortcutRecorder.tsx
  • src/components/ThemePicker.tsx
  • src/components/panes/AboutPane.tsx
  • src/components/panes/AppearancePane.tsx
  • src/components/panes/BehaviorPane.tsx
  • src/components/panes/ClockPane.tsx
  • src/components/ui/ColorField.tsx
  • src/components/ui/Field.tsx
  • src/components/ui/Segmented.tsx
  • src/components/ui/Select.tsx
  • src/components/ui/Slider.tsx
  • src/contexts/SettingsContext.tsx
  • src/dev-preview.tsx
  • src/globals.css
  • src/hooks/useClockDisplay.ts
  • src/utils/clockFormat.ts
  • src/vite-env.d.ts
  • vite.config.ts
💤 Files with no reviewable changes (1)
  • src/components/Settings.tsx

Comment thread pnpm-workspace.yaml
Comment on lines +1 to +2
allowBuilds:
esbuild: true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -nP '"packageManager"\s*:|pnpm@|corepack|pnpm/action-setup|pnpm-version' \
  --glob 'package.json' \
  --glob '*.yml' \
  --glob '*.yaml' \
  --glob '.nvmrc' \
  --glob '.tool-versions' \
  .

Repository: emmsixx/always-on-clock

Length of output: 161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate toolchain files ---'
git ls-files | rg '(^|/)(package\.json|pnpm-lock\.yaml|pnpm-workspace\.yaml|.*\.ya?ml|\.nvmrc|\.tool-versions|.*Dockerfile.*)$' | head -200

printf '%s\n' '--- package manager and pnpm references ---'
rg -n -i 'packageManager|pnpm|corepack|pnpm/action-setup|pnpm-version|strictDepBuilds|allowBuilds' . \
  --glob '!node_modules/**' --glob '!dist/**' --glob '!build/**' || true

Repository: emmsixx/always-on-clock

Length of output: 708


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- package.json ---'
cat -n package.json

printf '%s\n' '--- CI workflow ---'
cat -n .github/workflows/ci.yml

printf '%s\n' '--- release workflow ---'
cat -n .github/workflows/release.yml

Repository: emmsixx/always-on-clock

Length of output: 9751


🌐 Web query:

pnpm 9 allowBuilds setting unknown setting pnpm 10.26.0 strictDepBuilds

💡 Result:

The settings allowBuilds and strictDepBuilds are part of pnpm's build script security configuration and were introduced/formalized in pnpm v10 [1][2]. allowBuilds This setting was introduced in pnpm v10.26.0 [3][1]. It is defined in pnpm-workspace.yaml and provides a centralized, granular way to manage which packages are permitted to execute build scripts (such as postinstall) [3][1]. It accepts a map of package matchers (package name patterns) mapped to boolean values: true to explicitly allow execution, and false to disallow it [3][4]. This setting replaces several legacy options, including onlyBuiltDependencies, onlyBuiltDependenciesFile, neverBuiltDependencies, and ignoredBuiltDependencies [3][1][4]. strictDepBuilds This setting controls how pnpm handles dependencies that have unreviewed build scripts [2]. - When strictDepBuilds is true (the default in later versions), pnpm will fail the installation with an error if it encounters a dependency with an unreviewed build script that is not listed in allowBuilds [3][2][4]. - When strictDepBuilds is false, pnpm will instead print a warning and continue the installation without running the script [3][5]. Compatibility - These settings are not available in pnpm v9 [3][1][6]. If you attempt to use them in a version of pnpm that does not support them, pnpm may treat them as unknown settings or emit warnings [6]. - To migrate from pnpm v9, you should upgrade to pnpm v10.26.0 or later to utilize the allowBuilds configuration [1][5]. - For pnpm v11 and later, strictDepBuilds is true by default, and the legacy build-related settings have been removed [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- lockfile header ---'
sed -n '1,20p' pnpm-lock.yaml

printf '%s\n' '--- local-install documentation and version references ---'
rg -n -i 'pnpm|version|corepack|install' README.md package.json pnpm-lock.yaml .github/workflows \
  --glob '!**/node_modules/**' | head -120

Repository: emmsixx/always-on-clock

Length of output: 4813


Use pnpm 10.26.0 or later in CI, release, and local installs. Both workflows pin pnpm 9, which does not support allowBuilds. Add a packageManager pin to package.json and update both workflow pins; otherwise the esbuild approval is not applied.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pnpm-workspace.yaml` around lines 1 - 2, Pin the repository’s package manager
to pnpm 10.26.0 or later via the package.json packageManager field, and update
the pnpm versions used by both CI and release workflows to the same supported
version. Keep the existing allowBuilds configuration for esbuild unchanged.

Comment on lines +65 to +94
{confirmingReset ? (
<div className="confirm">
<button
type="button"
className="button button--ghost"
onClick={() => setConfirmingReset(false)}
>
Cancel
</button>
<button
type="button"
className="button button--danger"
onClick={() => {
void resetSettings().catch(() => undefined);
setConfirmingReset(false);
}}
>
Reset everything
</button>
</div>
) : (
<button
type="button"
className="button button--ghost"
onClick={() => setConfirmingReset(true)}
>
<RotateCcw size={13} strokeWidth={2.3} aria-hidden="true" />
Restore
</button>
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Focus is lost when the confirm state toggles.

The "Restore" button unmounts when confirmingReset becomes true, and the confirm buttons unmount when it returns to false. The browser moves focus to <body> each time. A keyboard user must tab back through the pane to reach the destructive action, and a screen reader announces nothing about the new choice.

Move focus to the "Cancel" button when the confirm state opens, and back to "Restore" when it closes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/panes/AboutPane.tsx` around lines 65 - 94, Update the
confirmingReset toggle in AboutPane so focus moves to the mounted Cancel button
when confirmation opens and returns to the Restore button when confirmation
closes. Add stable refs and an effect or equivalent focus management tied to the
state transition, ensuring each target is focused only after it is mounted while
preserving the existing reset behavior.

Comment on lines 262 to 266
{!isDefault && !isRecording && (
<button
type="button"
className="shortcut-recorder-reset"
onClick={() => onChange(defaultValue)}
>
<RotateCcw size={12} strokeWidth={2.3} aria-hidden="true" />
Reset
<button type="button" className="button button--link" onClick={() => onChange(defaultValue)}>
<RotateCcw size={11} strokeWidth={2.4} aria-hidden="true" />
Reset to default
</button>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle the reset save like the record save.

Line 263 calls onChange(defaultValue) and drops the returned promise. BehaviorPane returns updateSettings({ globalShortcut }), which rejects when the store write fails. Two consequences follow:

  • The rejection is unhandled, so it surfaces only as a console error.
  • feedback never changes, so the reset appears to do nothing and the user gets no explanation.

The reset also does not increment saveAttemptRef, so a stale record save can still write its own feedback afterwards. Reuse the save handling from lines 153-172.

🐛 Proposed reset handling
+  const applyShortcut = (shortcut: string) => {
+    const saveAttempt = ++saveAttemptRef.current;
+    setFeedback('Saving…');
+    void Promise.resolve()
+      .then(() => onChangeRef.current(shortcut))
+      .then(
+        () => {
+          if (saveAttempt === saveAttemptRef.current) {
+            setFeedback('Saved. This shortcut now toggles the clock.');
+          }
+        },
+        () => {
+          if (saveAttempt === saveAttemptRef.current) {
+            setFeedback('Could not save. The previous shortcut is still active.');
+          }
+        },
+      );
+  };
         {!isDefault && !isRecording && (
-          <button type="button" className="button button--link" onClick={() => onChange(defaultValue)}>
+          <button
+            type="button"
+            className="button button--link"
+            onClick={() => applyShortcut(defaultValue)}
+          >
             <RotateCcw size={11} strokeWidth={2.4} aria-hidden="true" />
             Reset to default
           </button>
         )}

Then call applyShortcut(parts.join('+')) in the keydown handler instead of the inline promise chain.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ShortcutRecorder.tsx` around lines 262 - 266, Update the reset
button in ShortcutRecorder to reuse the existing save-handling helper used by
the record flow, including saveAttemptRef invalidation and feedback/error
handling, instead of calling onChange(defaultValue) directly. Extract or reuse
an applyShortcut-style function and invoke it from both the reset handler and
keydown path, with the reset passing defaultValue so rejected saves are surfaced
to feedback.

Comment on lines +19 to +29
useEffect(() => {
setDraft(value);
}, [value]);

const handleTextChange = (next: string) => {
const normalized = next === '' ? '' : next.startsWith('#') ? next : `#${next}`;
setDraft(normalized);
if (HEX.test(normalized)) {
onChange(normalized.toLowerCase());
}
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The draft resets while the user types uppercase hex.

handleTextChange commits normalized.toLowerCase(). The parent then updates value, and the effect at lines 19-21 overwrites draft with the lowercase string. The visible text changes case mid-entry and the caret moves to the end. Sync the draft only when the incoming value differs from the normalized draft.

🐛 Proposed draft sync guard
   useEffect(() => {
-    setDraft(value);
+    setDraft((current) => (current.toLowerCase() === value.toLowerCase() ? current : value));
   }, [value]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
setDraft(value);
}, [value]);
const handleTextChange = (next: string) => {
const normalized = next === '' ? '' : next.startsWith('#') ? next : `#${next}`;
setDraft(normalized);
if (HEX.test(normalized)) {
onChange(normalized.toLowerCase());
}
};
useEffect(() => {
setDraft((current) => (current.toLowerCase() === value.toLowerCase() ? current : value));
}, [value]);
const handleTextChange = (next: string) => {
const normalized = next === '' ? '' : next.startsWith('#') ? next : `#${next}`;
setDraft(normalized);
if (HEX.test(normalized)) {
onChange(normalized.toLowerCase());
}
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/ColorField.tsx` around lines 19 - 29, Update the value
synchronization useEffect in ColorField so it only calls setDraft when the
incoming value differs from the current draft after normalization, preserving
the user’s original casing and caret during uppercase hex entry. Keep
handleTextChange’s lowercase onChange behavior unchanged.

Comment thread src/components/ui/Field.tsx Outdated
Comment on lines +17 to +40
export const Field: React.FC<RowProps> = ({ label, hint, control, children, htmlFor }) => {
const generatedId = useId();
const hintId = hint ? `${generatedId}-hint` : undefined;

return (
<div className={`field ${children ? 'field--stacked' : ''}`.trim()}>
<div className="field-head">
<div className="field-copy">
{htmlFor ? (
<label className="field-label" htmlFor={htmlFor}>
{label}
</label>
) : (
<span className="field-label">{label}</span>
)}
{hint && (
<span className="field-hint" id={hintId}>
{hint}
</span>
)}
</div>
{control && <div className="field-control">{control}</div>}
</div>
{children && <div className="field-body">{children}</div>}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The hint id is generated but never associated with a control.

hintId is applied to the hint span, and no element references it through aria-describedby. Screen reader users hear the label without the hint. Expose the id so the control can consume it.

♿ Proposed fix
-export const Field: React.FC<RowProps> = ({ label, hint, control, children, htmlFor }) => {
+export const Field: React.FC<RowProps> = ({ label, hint, control, children, htmlFor }) => {
   const generatedId = useId();
   const hintId = hint ? `${generatedId}-hint` : undefined;

Then set aria-describedby={hintId} on the rendered control. One option is to change children and control to accept a callback:

-  /** Rendered full width beneath the label (segmented controls, sliders, grids). */
-  children?: React.ReactNode;
+  /** Rendered full width beneath the label. Receives the hint id for `aria-describedby`. */
+  children?: React.ReactNode | ((hintId: string | undefined) => React.ReactNode);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/Field.tsx` around lines 17 - 40, Update the Field
component’s control rendering so the generated hintId is passed to the
associated control through aria-describedby. Ensure both the control prop and
children path can consume this association, preserving the existing hintId
generation and rendering behavior.

Comment on lines +226 to 261

const updateSettings = useCallback(async (updates: Partial<Settings>) => {
const previousSettings = settingsRef.current;
const nextSettings = { ...previousSettings, ...updates };
const updateVersion = ++settingsUpdateVersionRef.current;
settingsRef.current = nextSettings;
setSettings(nextSettings);
setSaveError(null);

const saveOperation = saveOperationRef.current
.catch(() => undefined)
.then(() => saveSettings(updates));
saveOperationRef.current = saveOperation;

try {
await saveSettings(updates);
await saveOperation;
} catch (err) {
settingsRef.current = previousSettings;
setSettings(previousSettings);
if (
updateVersion === settingsUpdateVersionRef.current &&
settingsRef.current === nextSettings
) {
settingsRef.current = previousSettings;
setSettings(previousSettings);
setSaveError('Could not save changes. Try again.');
}
throw err;
}

setLastSavedAt(Date.now());

try {
await emit(SETTINGS_UPDATED_EVENT, updates);
} catch (err) {
console.warn('Failed to synchronize settings between windows:', err);
}
}, []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Roll back only the keys that failed to save.

updateSettings snapshots the whole settings object and restores it on failure. If a cross-window settings-updated event or a store onChange applies unrelated keys while the save is in flight, two outcomes are possible:

  • The external update renders first. settingsRef.current no longer equals nextSettings, so the rollback is skipped and the failed values stay visible.
  • The external update has not yet reached settingsRef (the sync effect at lines 55-57 runs after render). The rollback then discards the external keys as well.

A key-scoped rollback avoids both cases.

🐛 Proposed key-scoped rollback
     } catch (err) {
-      if (
-        updateVersion === settingsUpdateVersionRef.current &&
-        settingsRef.current === nextSettings
-      ) {
-        settingsRef.current = previousSettings;
-        setSettings(previousSettings);
-        setSaveError('Could not save changes. Try again.');
-      }
+      if (updateVersion === settingsUpdateVersionRef.current) {
+        const reverted = (Object.keys(updates) as Array<keyof Settings>).reduce<Partial<Settings>>(
+          (acc, key) => Object.assign(acc, { [key]: previousSettings[key] }),
+          {},
+        );
+        settingsRef.current = { ...settingsRef.current, ...reverted };
+        setSettings((current) => ({ ...current, ...reverted }));
+        setSaveError('Could not save changes. Try again.');
+      }
       throw err;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const updateSettings = useCallback(async (updates: Partial<Settings>) => {
const previousSettings = settingsRef.current;
const nextSettings = { ...previousSettings, ...updates };
const updateVersion = ++settingsUpdateVersionRef.current;
settingsRef.current = nextSettings;
setSettings(nextSettings);
setSaveError(null);
const saveOperation = saveOperationRef.current
.catch(() => undefined)
.then(() => saveSettings(updates));
saveOperationRef.current = saveOperation;
try {
await saveSettings(updates);
await saveOperation;
} catch (err) {
settingsRef.current = previousSettings;
setSettings(previousSettings);
if (
updateVersion === settingsUpdateVersionRef.current &&
settingsRef.current === nextSettings
) {
settingsRef.current = previousSettings;
setSettings(previousSettings);
setSaveError('Could not save changes. Try again.');
}
throw err;
}
setLastSavedAt(Date.now());
try {
await emit(SETTINGS_UPDATED_EVENT, updates);
} catch (err) {
console.warn('Failed to synchronize settings between windows:', err);
}
}, []);
const updateSettings = useCallback(async (updates: Partial<Settings>) => {
const previousSettings = settingsRef.current;
const nextSettings = { ...previousSettings, ...updates };
const updateVersion = ++settingsUpdateVersionRef.current;
settingsRef.current = nextSettings;
setSettings(nextSettings);
setSaveError(null);
const saveOperation = saveOperationRef.current
.catch(() => undefined)
.then(() => saveSettings(updates));
saveOperationRef.current = saveOperation;
try {
await saveOperation;
} catch (err) {
if (updateVersion === settingsUpdateVersionRef.current) {
const reverted = (Object.keys(updates) as Array<keyof Settings>).reduce<Partial<Settings>>(
(acc, key) => Object.assign(acc, { [key]: previousSettings[key] }),
{},
);
settingsRef.current = { ...settingsRef.current, ...reverted };
setSettings((current) => ({ ...current, ...reverted }));
setSaveError('Could not save changes. Try again.');
}
throw err;
}
setLastSavedAt(Date.now());
try {
await emit(SETTINGS_UPDATED_EVENT, updates);
} catch (err) {
console.warn('Failed to synchronize settings between windows:', err);
}
}, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/contexts/SettingsContext.tsx` around lines 226 - 261, Update
updateSettings to roll back only the keys included in updates when saveOperation
fails, rather than restoring the entire previousSettings object or requiring
settingsRef.current === nextSettings. Preserve unrelated settings changes
applied by cross-window synchronization or store onChange, while clearing the
save error and restoring the failed keys from their pre-update values only when
this update remains the latest relevant version.

Comment thread src/dev-preview.tsx
Comment on lines +26 to +40
invoke: async (cmd: string, args: Record<string, unknown> = {}) => {
switch (cmd) {
case 'plugin:store|load':
return 1;
case 'plugin:store|get':
return [store.get(args.key as string), store.has(args.key as string)];
case 'plugin:store|set':
store.set(args.key as string, args.value);
return undefined;
case 'plugin:event|listen':
return 1;
case 'plugin:window|is_always_on_top':
return false;
default:
return undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not silently accept unsupported browser commands.

Lines 26-40 return undefined for every command outside the switch. src/SettingsWindow.tsx calls getCurrentWindow().close() for its close button, and plugin:event|listen returns 1 without an event emission path. These calls look successful while doing nothing in the browser preview. Add explicit stubs for interactions that this harness promises to drive, or throw for unsupported commands.

Expose unsupported commands
       default:
-        return undefined;
+        throw new Error(`Unsupported Tauri command in browser preview: ${cmd}`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dev-preview.tsx` around lines 26 - 40, Update the invoke handler in
src/dev-preview.tsx so unsupported browser commands no longer fall through to a
silent undefined result. Add explicit stubs for promised interactions such as
the SettingsWindow close command and an event-listening path that can emit
events, and make the default branch throw for any remaining unsupported command.

Comment thread src/globals.css
Comment on lines +10 to +47
--font-ui: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

--bg-chrome: #0b0c0e;
--bg-canvas: #121316;
--bg-raised: #1a1d21;
--bg-raised-hover: #22262b;
--bg-inset: #08090b;

--line: rgba(255, 255, 255, 0.07);
--line-strong: rgba(255, 255, 255, 0.12);

--fg: #e9ebef;
--fg-2: #a0a6b0;
--fg-3: #7e8794;

--accent: #4f7cf7;
--accent-bright: #9bbaff;
--accent-tint: rgba(79, 124, 247, 0.15);
--accent-ring: rgba(106, 155, 255, 0.5);

--ok: #5fd3a0;
--danger: #f0736b;
--danger-tint: rgba(240, 115, 107, 0.13);

--r-xs: 6px;
--r-sm: 8px;
--r-md: 10px;
--r-lg: 13px;

--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
--dur-fast: 120ms;
--dur: 180ms;
--dur-slow: 260ms;

color-scheme: dark;
font-family: var(--font-ui);
font-synthesis: none;
text-rendering: optimizelegibility;
text-rendering: optimizeLegibility;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep this casing and relax the Stylelint rule instead.

Stylelint flags BlinkMacSystemFont (line 10) and optimizeLegibility (line 47) under value-keyword-case. Both values are case-sensitive. Lowercasing them breaks the font stack and produces an invalid text-rendering keyword. Add both to ignoreKeywords so the lint job passes without a behavior change.

🔧 Proposed Stylelint config change
{
  "rules": {
    "value-keyword-case": [
      "lower",
      { "ignoreKeywords": ["BlinkMacSystemFont", "optimizeLegibility"] }
    ]
  }
}
🧰 Tools
🪛 Stylelint (17.14.0)

[error] 10-10: Expected "BlinkMacSystemFont" to be "blinkmacsystemfont" (value-keyword-case)

(value-keyword-case)


[error] 47-47: Expected "optimizeLegibility" to be "optimizelegibility" (value-keyword-case)

(value-keyword-case)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/globals.css` around lines 10 - 47, Update the Stylelint
value-keyword-case configuration to ignore the case-sensitive keywords
BlinkMacSystemFont and optimizeLegibility, preserving their casing in the
--font-ui and text-rendering declarations without changing runtime behavior.

Source: Linters/SAST tools

Comment thread src/utils/clockFormat.ts
Comment on lines +6 to +18
export function formatTime(now: Date, { timeFormat, showSeconds }: TimeOptions): string {
const options: Intl.DateTimeFormatOptions = {
hour: '2-digit',
minute: '2-digit',
hour12: timeFormat === '12h',
};

if (showSeconds) {
options.second = '2-digit';
}

return now.toLocaleTimeString('en-US', options);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Intl.DateTimeFormat hour 2-digit hour12 true leading zero en-US

💡 Result:

When using Intl.DateTimeFormat with hour set to '2-digit' and hour12 set to true (which is the default for 'en-US'), JavaScript engines currently do not consistently apply a leading zero to the hour [1][2]. While '2-digit' is intended to force a two-digit representation (e.g., '01', '09'), many implementations ignore this requirement when combined with a 12-hour clock (hour12: true) [1][2]. This is a long-standing known issue in various environments, including Chrome, Node.js, and others, where single-digit hours are rendered without a leading zero [1][2]. If you require a leading zero for 12-hour time formats, the most reliable approach is to use formatToParts to manually construct the string or to pad the hour value yourself [2][3]. For example: const date = new Date; const parts = new Intl.DateTimeFormat('en-US', { hour: '2-digit', hour12: true }).formatToParts(date); const hour = parts.find(p => p.type === 'hour').value.padStart(2, '0'); const ampm = parts.find(p => p.type === 'dayPeriod').value; console.log(${hour} ${ampm}); This behavior works as expected (with leading zeros) when hour12 is set to false (24-hour format) [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate files ---'
fd -i 'clockFormat|ClockPane' . || true

printf '%s\n' '--- formatTime implementation ---'
format_file="$(fd -i -t f 'clockFormat' . | head -n 1)"
cat -n "$format_file"

printf '%s\n' '--- ClockPane sample references ---'
pane_file="$(fd -i -t f 'ClockPane' . | head -n 1)"
rg -n -C 4 '2:45|12h|timeFormat|formatTime' "$pane_file" || true

printf '%s\n' '--- runtime Intl behavior ---'
node - <<'JS'
const dates = [
  new Date(2024, 0, 1, 2, 45, 0),
  new Date(2024, 0, 1, 12, 45, 0),
];
for (const date of dates) {
  for (const hour12 of [true, false]) {
    const options = { hour: '2-digit', minute: '2-digit', hour12 };
    console.log(JSON.stringify({
      date: date.toString(),
      hour12,
      output: date.toLocaleTimeString('en-US', options),
      numeric12: hour12
        ? date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12 })
        : undefined,
    }));
  }
}
JS

Repository: emmsixx/always-on-clock

Length of output: 3166


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- project runtime and browser targets ---'
for f in package.json package-lock.json pnpm-lock.yaml yarn.lock vite.config.ts vite.config.js; do
  if [ -f "$f" ]; then
    printf '\n--- %s ---\n' "$f"
    rg -n -C 3 'engines|browserslist|target|browser|vite|electron|chrome|firefox|safari' "$f" || true
  fi
done

printf '%s\n' '--- formatTime usage and tests ---'
rg -n -C 5 'formatTime|2:45 PM|02:45 AM' src test tests . --glob '!node_modules' --glob '!dist' --glob '!build' 2>/dev/null || true

Repository: emmsixx/always-on-clock

Length of output: 50379


Use numeric for 12-hour output.

V8/ICU formats hour: '2-digit' with hour12: true as 02:45 AM, while ClockPane.tsx advertises 2:45 PM. Set hour to hour12 ? 'numeric' : '2-digit' and reuse hour12.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/clockFormat.ts` around lines 6 - 18, Update formatTime to derive an
hour12 boolean from timeFormat and reuse it for both the hour option and hour12
setting; use numeric hours when hour12 is true and 2-digit hours otherwise,
preserving the existing minute and seconds formatting.

@emmsixx
emmsixx merged commit 240d29f into codex/shortcut-recorder Aug 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant