Skip to content

feat(reports): counter for meeting attendance records#5291

Open
ux-git wants to merge 14 commits into
mainfrom
feat/clicker-counter-mode
Open

feat(reports): counter for meeting attendance records#5291
ux-git wants to merge 14 commits into
mainfrom
feat/clicker-counter-mode

Conversation

@ux-git

@ux-git ux-git commented Jul 14, 2026

Copy link
Copy Markdown
Member

Description

Adds Counter — a touch-friendly way to tally meeting attendance on phones and tablets, so attendance can be counted without watching the device screen.

ezgif-14c5f1a74b38bffd.gif

What was added

  • Full-screen overlay with a rolling odometer display, a large +/− pad, reset, save, and cancel.
  • Present / Online counts crossfade so they read as two separate counters; a horizontal swipe (touch and mouse) switches between them.
  • Atomic save of both counts in a single record write (meetingAttendanceCountsSave) — avoids dropping a value when present + online are saved together.
  • Shown only on compact viewports or touch devices (useIsTouchDevice), including large iPads whose width reads like a laptop's.
  • Haptic feedback on each count, on the 1914 cap refusal, and on reset — so a tally registers by feel rather than by looking (see below).

Counting reliability & polish

  • Counting registers on pointer-down instead of click. A click is swallowed when focus has just moved — most visibly, the first tap after switching Present/Online tabs did nothing. That is unacceptable for a control designed to be used without looking, so presses now fire immediately, like a physical tally clicker. Keyboard activation still works (it arrives as a click with detail === 0), and the tiles opt out of tap delay and double-tap zoom.
  • The reset arrow is mirrored to point counter-clockwise, spins in that same direction, and now stays on screen for its full 500ms spin before fading — previously it vanished the instant the count hit 0, so the animation was never really seen.
  • Decrementing at 0 is a no-op instead of marking the tab as touched.

New reusable global components

Two general-purpose components were added under src/components/, intended to be reused across other pages, not just here:

TabSwitcher

A generic segmented control (iOS-style toggle):

  • Icon + label per option with a sliding active-highlight indicator.
  • Ellipsis-truncates long / translated labels; equal-width segments.
  • Generic over the value type for exact value / onChange typing.
  • Works for any 2+ options — reuse it for list filters, view toggles, etc.

SubpageNavbar

The app's subpage navigation bar (back button, title, subtitle, optional trailing action), extracted into a reusable component so overlays/dialogs that live outside the router can use it too:

  • Responsive: title centered on mobile, left-aligned from tablet688 up.
  • When no secondaryTitle is passed, it inherits the parent page's title from navBarOptionsState (single source of truth), so it can't drift when the page is renamed.

Merge order: this is the same bar the app's main navbar renders inline today (and that #5288 reuses for the field-service subpage). A follow-up will migrate the navbar to render SubpageNavbar so there's a single implementation — so this PR is upstream and should be merged first.

New reusable global service

@services/haptics

Haptics is introduced here as a shared, app-wide service, not as clicker-only code — the counter is simply its first consumer:

  • Call sites use semantic intents (haptic('tap'), haptic('limit'), haptic('reset')) rather than raw millisecond arrays. Patterns live in one table (patterns.ts), so the feel of every surface is tuned in a single place and new surfaces reuse existing intents.
  • A single shared instance, gated centrally by device capability and by the user setting — so no call site has to check anything.
  • Never throws. Haptics are a non-essential enhancement and must never break the interaction they decorate.
  • Patterns are deliberately tied to the visuals they accompany: the cap-refusal buzz mirrors the 400ms "denied" shake (four decaying jolts, one every 80ms), and reset decays across the 500ms spin before landing a firmer pulse as the count reaches 0.

New user settingHaptic feedback under Organized settings (directly below automatic theme change), shown only on touch devices since vibration cannot exist elsewhere. Defaults on. Registered through the usual path: user_settings type + schema default, jotai atom, encryption map, and the pocket backup payload.

Why a dependency (web-haptics, MIT): the Vibration API has no amplitude control, and iOS Safari does not implement it at all. The library simulates intensity via PWM on Android and drives the iOS 17.4+ switch-toggle fallback — both of which we would otherwise have to hand-roll. It is wrapped behind our own service so the engine stays swappable.

Type of change

  • New feature (non-breaking change which adds functionality)

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • Any dependent changes have been merged and published in downstream modules

Notes for reviewers

  • ESLint clean; tsc --noEmit introduces 0 new errors (repo baseline unchanged); production build passes.
  • tr_clickerMode = "Counter" (plus tr_increase / tr_decrease, tr_hapticFeedback) added to the English source; other locales flow through Crowdin.
  • Accessibility: the dialog has an accessible name, the decorative odometer ribbon is aria-hidden with the real value exposed to screen readers, and the +/− controls have localized labels.
  • One new dependency: web-haptics (MIT). Rationale above.
  • Haptics can only be felt on physical hardware. The Android path is the reliable one; the iOS switch-toggle fallback is best-effort and has not been verified on a physical iPhone — worth a check before release. iPads match the touch query but have no vibration motor, and the web cannot distinguish them from iPhones, which is why the setting's description says "if your device and browser support it".

Adds a touch-friendly counter for tallying meeting attendance on phones,
tablets, and narrow desktop windows. Focusing a count field reveals a
"Counter" suggestion button that opens a full-screen overlay with large
+/- controls, so attendance can be tapped in without the keyboard.

- Reusable components: TabSwitcher (segmented control with icon/label and
  ellipsis handling) and SubpageHeader (overlay top bar that inherits the
  parent page title so it can't drift)
- Odometer display with per-digit ribbon roll, leading-zero collapse, and
  a count-down on reset
- Present/Online counts crossfade so they read as separate counters, with
  swipe-to-switch (touch and mouse)
- Atomic save of both counts in one write (meetingAttendanceCountsSave)
- Shown only on compact viewports or touch devices (useIsTouchDevice),
  including large iPads
@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
staging-organized-app Ready Ready Preview Jul 20, 2026 4:02pm
test-organized-app Ready Ready Preview Jul 20, 2026 4:02pm

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds a clicker-mode attendance editor with animated counters, touch and swipe interactions, reusable navigation controls, field suggestions, haptic feedback settings, and atomic persistence for present and online attendance counts.

Changes

Clicker attendance flow

Layer / File(s) Summary
Shared headers and tab controls
src/components/subpage_navbar/*, src/components/tab_switcher/*
Adds typed SubpageNavbar and generic TabSwitcher components with accessible navigation, responsive layout, icons, labels, and sliding selection highlighting.
Clicker state and animated controls
src/features/reports/meeting_attendance/monthly_record/clicker_mode/{index.types.ts,useClickerMode.tsx,index.styles.ts}, animated_count/*, count_swap/*, counter_pad/*, confetti/*, suggestion_button/*
Adds clicker state, count limits, touched-tab tracking, animated digit rendering, tab transitions, reset and increment controls, milestone confetti, and field suggestion UI.
Clicker mode overlay
src/features/reports/meeting_attendance/monthly_record/clicker_mode/index.tsx
Adds a slide-in dialog with tab selection, pointer swipe navigation, animated count/reset controls, counter actions, and Save/Cancel handling.
Week box and touch integration
src/features/reports/meeting_attendance/monthly_record/week_box/*, src/hooks/*
Enables clicker mode for touch-capable or narrow layouts, tracks focused attendance fields, renders suggestions, opens the overlay with normalized values, and updates input alignment.
Atomic attendance persistence
src/services/app/meeting_attendance.ts
Centralizes writable attendance-row creation and adds a multi-count save operation that persists present and online values in one database write.
Haptics preference and feedback
src/services/haptics/*, src/states/settings.ts, src/features/my_profile/app_settings/*, src/services/dexie/schema.ts, src/services/worker/backupUtils.ts, src/definition/settings.ts, src/constants/table_encryption_map.ts
Adds persisted touch-device haptics settings, semantic vibration patterns, settings UI, backup export support, and clicker tap, limit, and reset feedback.

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

Sequence Diagram(s)

sequenceDiagram
  participant WeekBox
  participant ClickerSuggestion
  participant ClickerMode
  participant useClickerMode
  participant AttendanceService
  participant Haptics
  WeekBox->>ClickerSuggestion: show focused-field suggestion
  ClickerSuggestion->>ClickerMode: open clicker dialog
  ClickerMode->>useClickerMode: edit active attendance count
  useClickerMode->>Haptics: trigger tap, limit, or reset intent
  useClickerMode->>ClickerMode: return count and interaction signals
  ClickerMode->>WeekBox: submit touched present/online values
  WeekBox->>AttendanceService: save counts for the attendance row
Loading

Possibly related PRs

Suggested labels: released

Suggested reviewers: rhahao

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title matches the main change: adding a meeting-attendance counter feature in reports.
Description check ✅ Passed The description is clearly aligned with the PR and accurately summarizes the new counter, haptics, and reusable components.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/clicker-counter-mode

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.

coderabbitai[bot]

This comment was marked as resolved.

- Dialog: add aria-label so the full-screen overlay has an accessible name
- AnimatedCount: hide the odometer ribbon from the a11y tree and expose the
  real numeric value via a visually-hidden element
- CounterPad: localize the +/- aria-labels (tr_increase / tr_decrease)
- SubpageHeader: make backLabel required so the back button always has a name
- AnimatedCount: use nullish assignment for the tween start timestamp
Keep only short notes in the complex spots (odometer ribbon, crossfade,
swipe); remove the obvious ones. No behavior change.
Odometer ribbon cells keyed by a stable id; confetti pieces by their unique
start column. Addresses Sonar 'Do not use Array index in keys'.
@ux-git ux-git changed the title feat(reports): new clicker counter mode for meeting attendance records feat(reports): counter for meeting attendance records Jul 14, 2026
The icon already existed on main as plain paths (one of ~29 mask-less icons).
The added <mask>/<g> was a full-coverage no-op — reverting removes the diff
noise and the duplicate-id concern, leaving a pre-existing file untouched.
Remove prop-by-prop JSDoc from the type files and other self-evident
comments; keep only the non-obvious 'why' notes. No behavior change.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 14, 2026
@mergify

mergify Bot commented Jul 14, 2026

Copy link
Copy Markdown

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

Condense the kept 'why' notes to single tight lines; no behavior change.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 14, 2026
@ux-git
ux-git force-pushed the feat/clicker-counter-mode branch from 5af9bb2 to c6b73f6 Compare July 14, 2026 16:57

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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
`@src/features/reports/meeting_attendance/monthly_record/clicker_mode/animated_count/index.tsx`:
- Around line 211-218: Update the visually hidden count element in the animated
count component to include an appropriate aria-live attribute so screen readers
announce changes to display as it updates. Keep the existing visuallyHidden
styling and rendered value unchanged.

In `@src/features/reports/meeting_attendance/monthly_record/week_box/index.tsx`:
- Around line 65-86: Move the blur handling from each TextField to its wrapping
Box in both the present-field block
(src/features/reports/meeting_attendance/monthly_record/week_box/index.tsx#L65-L86)
and online-field block
(src/features/reports/meeting_attendance/monthly_record/week_box/index.tsx#L98-L119).
Update the container-level handlers to inspect event.relatedTarget and avoid
calling handleFieldBlur when focus moves to the ClickerSuggestion button within
that same container.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d11fe56b-daa6-4e69-ba29-06a169fe970d

📥 Commits

Reviewing files that changed from the base of the PR and between 5af9bb2 and 6a20645.

⛔ Files ignored due to path filters (1)
  • src/locales/en/meetings.json is excluded by !**/*.json
📒 Files selected for processing (23)
  • src/components/subpage_navbar/index.tsx
  • src/components/subpage_navbar/index.types.ts
  • src/components/tab_switcher/index.tsx
  • src/components/tab_switcher/index.types.ts
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/animated_count/index.tsx
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/animated_count/index.types.ts
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/confetti/index.tsx
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/count_swap/index.tsx
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/count_swap/index.types.ts
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/counter_pad/index.tsx
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/counter_pad/index.types.ts
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/index.styles.ts
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/index.tsx
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/index.types.ts
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/suggestion_button/index.tsx
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/suggestion_button/index.types.ts
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/useClickerMode.tsx
  • src/features/reports/meeting_attendance/monthly_record/week_box/index.styles.ts
  • src/features/reports/meeting_attendance/monthly_record/week_box/index.tsx
  • src/features/reports/meeting_attendance/monthly_record/week_box/useWeekBox.tsx
  • src/hooks/index.ts
  • src/hooks/useIsTouchDevice.tsx
  • src/services/app/meeting_attendance.ts
🚧 Files skipped from review as they are similar to previous changes (19)
  • src/hooks/index.ts
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/animated_count/index.types.ts
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/count_swap/index.types.ts
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/counter_pad/index.types.ts
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/suggestion_button/index.types.ts
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/confetti/index.tsx
  • src/components/tab_switcher/index.types.ts
  • src/features/reports/meeting_attendance/monthly_record/week_box/index.styles.ts
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/suggestion_button/index.tsx
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/useClickerMode.tsx
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/index.styles.ts
  • src/hooks/useIsTouchDevice.tsx
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/index.tsx
  • src/features/reports/meeting_attendance/monthly_record/week_box/useWeekBox.tsx
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/index.types.ts
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/count_swap/index.tsx
  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/counter_pad/index.tsx
  • src/components/tab_switcher/index.tsx
  • src/services/app/meeting_attendance.ts

Comment thread src/features/reports/meeting_attendance/monthly_record/week_box/index.tsx Outdated
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 2 file(s) based on 2 unresolved review comments.

Files modified:

  • src/features/reports/meeting_attendance/monthly_record/clicker_mode/animated_count/index.tsx
  • src/features/reports/meeting_attendance/monthly_record/week_box/index.tsx

Commit: 6d04f1523b2e6a50943fa6d889a4e0ff22552e14

The changes have been pushed to the feat/clicker-counter-mode branch.

Time taken: 2m 17s

Fixed 2 file(s) based on 2 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
@socket-security

socket-security Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedweb-haptics@​0.0.67310010083100

View full report

coderabbitai[bot]

This comment was marked as resolved.

Counting now registers on pointer-down rather than click, so the first tap
after switching tabs is no longer swallowed. This matters because the whole
point of clicker mode is tallying without looking at the screen. Keyboard
activation still works through the click path (detail === 0), and the tiles
opt out of tap delay and double-tap zoom.

The reset arrow is mirrored to point counter-clockwise, spins in the matching
direction, and now stays on screen for the full 500ms spin before fading, so
the animation can actually be seen. Decrementing at 0 is a no-op instead of
marking the tab touched.

Adds a shared haptics service (@services/haptics) wrapping web-haptics, with
semantic intents rather than raw millisecond arrays: a tap per count, a
denial pattern mirroring the 400ms shake when the 1914 cap is refused, and a
decaying buzz plus final pulse for reset. Gated centrally by a new
haptics_enabled user setting, which is only shown on touch devices.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 19, 2026
ux-git added 2 commits July 20, 2026 00:10
The reset's closing pulse landed at 550ms, which is exactly when the
odometer finishes tweening to 0, so it read as arriving after the fact.
The decay is now compressed and the pulse fires at ~420ms, while the
digits are still settling onto 0.

Reaching 1914 also plays a light six-impulse "prrr" alongside the
confetti burst, fired from the same place the burst is triggered so the
two can't drift apart.
The 22px spacer that balances the back button held its width even while
the title was truncating, which long translations noticed. It now shrinks
away before the title has to truncate: the title takes flex-basis auto so
an oversized title actually overflows the row and triggers a shrink pass,
and the spacer carries a high shrink factor so it yields first.

Nothing moves while the title fits, so short titles stay centred exactly
as before; a long title gains the full 22px.
The counter tiles and reset button took their pressed look from CSS
:active, which the browser skips for the first press right after focus
moves (switching tabs). Drive the pressed state from pointer events via
an is-pressed class so that press is styled as clicked too.
@sonarqubecloud

Copy link
Copy Markdown

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