Tick Tock
- 52 Devlogs
- 85 Total hours
Tools about Time
Tools about Time
alarm.wav and notif.wav) from the static /public/sounds/ directory into the source-managed asset pipeline at src/assets/sounds/."/public/sounds/...") with explicit ES module imports (import alarmSoundUrl from '../assets/sounds/alarm.wav' and import notifSoundUrl from '../assets/sounds/notif.wav') across timer.js and pomodoro.js.formatLapsOutput: a dedicated text formatter that converts active stopwatch data into structured tab-separated values (TSV) suitable for copying.async wrapper using navigator.clipboard.writeText() with built-in error handling (try/catch) and user alerts for unsupported environments or clipboard permission rejections.copyBtn click events to trigger text copying and dynamically update the button icon (check or error) and theme accent color.querySelector in updateListedStopwatch from target class .listed-stopwatch-demo to parent wrapper .listed-stopwatch[data-id="..."], resolving element lookup failures during live updates.index.astro and BaseLayout.astro.pauseDuration calculation Date.now() - pauseStartTime during resume actions status === "paused". Shifted all existing lap timestamps lap.endTime += pauseDuration forward to subtract paused time intervals.handleLap, renderListedStopwatch, updateListedStopwatch, and updateCardUI using ternary state checks:
const now = stopwatch.status === "running" ? Date.now() : stopwatch.pauseStartTime;
-Replaced laps.forEach with for (let j = 0; j <= i; j++) to ensure cumulative total times (totalMS) strictly correspond to the current lap index.
formatTime() rendering logic by applying inline styles width, transform: scaleX(0.8), text-align: center to dynamic span elements, preventing font-width shifting during rapid ticker updates.updateListedStopwatch() into the global interval loop, enabling sidebar preview labels to tick synchronously with active main cards.laps?.[0]?.startTime across rendering pipelines to prevent runtime exceptions when initializing empty stopwatches.stopwatch schema tracking dynamic state (idle | running | paused) and an array of individual lap records (laps).Date.now() - lap.startTime), eliminating redundant accumulator fields like startTime or elapsedTime.handleStart, handlePause, handleReset, handleLap, and handleDelete.updateCardUI and updateCardBtns to reflect running, paused, and idle UI visual feedback dynamically.updateCardLapGrid using index-based duration summation for lap and total times.startGlobalTick() timer intervals and list reordering via handleMoveStopwatch().index.astro)..stopwatch-adding-action-btn and .stopwatch-list-container for dynamic item injection.#listed-stopwatch-template and #stopwatch-card-template)..stopwatch-text and .stopwatch-text-sub..stopwatch-card-action-btn) supporting Start, Pause, Replay, and Lap actions..stopwatch-lap-container) to organize lap metrics (i, lap, total).tool-card layout with hourglass Material symbol).BaseLayout.astro.feature/timer-main into main, bringing in the new /timer page route, timer.js logic, sound assets (alarm.wav), and updated layout components.YEAYYY
.sort-action control group with .sort-up and .sort-down arrow buttons to the template layout.handleMoveTimer(id, direction) to handle array element swapping with upper and lower boundary bounds checks.renderListedTimer() to execute reordering operations, trigger LocalStorage updates, and instantly re-render the UI layout via renderAll()..timer-text-sub element inside the ring container to preserve and display the initial totalSeconds value.var(--text-muted)..is-running state toggle based on timer.status === "running".@keyframes timer-text-flash pulsing animation to provide continuous visual feedback while countdown is active.new Audio() allowing each finished timer to play independent looping sound effects concurrently.handleReset() and handleDelete() .saveTimers() by stripping transient DOM/Audio references (alarmAudio) to prevent JSON stringification crashes.typeof alarmAudio.pause === "function") inside stopAlarm() to prevent runtime TypeError issues during state rehydration.renderAll() to resume alarm sound playback and background pulse animation for ended timers across page reloads.endTime correctly during timer reset to normalize state lifecycle.alarm.wav
alarm.wav for the alarming when timer.status==="ended".max-width, margin: 0, and padding: 0 constraints to .is-hidden to smoothly collapse hidden buttons without leaving blank layout gaps.max-width boundary to allow width-based CSS transitions.much smoother ♪(´▽`)
saveTimers() and loadTimers() for LocalStorage persistence.setupArrowBtns() to handle unit value increment and decrement bounds.<template> elements to dynamically generate sidebar list items and main cards via renderAll().handleDelete() to update state and UI.startGlobalTick() loop running every 200ms using endTime offsets to prevent background tab drift.strokeDashoffset dynamically based on remaining time ratios.<template> tags inside slot="template-content".id="timer-container"..textContent.textContent to .textContent
Data.now() to Date.now().timers.status to timer.status.finally got multi-timer state and progress rings syncing properly… bugs fixed and back to smooth sailing…
tabular-nums formatting to eliminate number jitter.the ui was so tiring… i DID remember to open a new branch (feature/timer-main) …
status: 'running') preventing automatic transition from Focus to Break. Added explicit status resetting in handlePhaseEnd before invoking startTimer.Math.round with Math.ceil in countdown logic to eliminate early sound triggers caused by rounding errors.endTime re-calculation when switching modes (work ↔ break) to maintain state consistency.public/sounds/notification.mp3.endTime - Date.now()) to maintain countdown accuracy during background tab throttling or page reloads.#menu-start-btn, #cycles-input).ReferenceError during event initialization by enforcing state argument scoping within bindEvents.loadStateFromLocalStorage parsing logic to filter "undefined" string literals and clear corrupted cache gracefully..timeline-track) with flex-proportional work/break segments and a dynamic progress pin (.timeline-pin).workDuration, breakDuration, timeRemaining) to ensure clean state initialization.Height = 140 * Count + 180)..export-canvas to ensure optimal vertical spacing regardless of selected cards count.feature/export-image branch into main after verifying clean template rendering and correct node referencing..export-canvas)..progress-card elements into an isolated container (.export-cards-holder), ensuring the on-screen UI remains completely untouched and non-flickering during image generation.ReferenceError caused by leftover references to targetContainer and stampEl inside initExportImageFeature.html2canvas target from main dashboard container to the temporary canvasEl.try...catch block to guarantee off-screen elements are properly garbage collected.html2canvas library to enable one-click PNG image export of the active progress dashboard.--bg-app).<template> element (#export-watermark-template), cleanly separating DOM structure from JavaScript execution logic.now.toLocaleString() to inject real-time snapshot dates and times into exported images.ignoreElements filtering in html2canvas to prevent unchecked/hidden .progress-card elements from creating top/bottom visual gaps in exported images.TypeError on insertBefore/appendChild by referencing clone.firstElementChild to extract the correct Element Node from DocumentFragment.export-watermark-template) and method call mismatches to ensure reliable script execution.#year,month,...) and localStorage synchronization into time-progress.js to preserve users’ toggle preferences across reloads and shares.hashchange event listener to ensure UI check states dynamically update when users use browser navigation (Back/Forward).#share-menu-btn event listener into BaseLayout.astro, eliminating duplicate share logic across individual tool pages.document.title dynamically to provide clean, contextual titles for native Web Share API and clipboard copy fallbacks.#head, #header-content, #custom-menu, and #main-content).--accent-color, --component-bg-standard, --text-heading), ensuring native support for Dark and Light mode toggling.initSidebarToggles, calculateTimeProgress, updateTicksUI, updateProgressCardUI, and updateSingleCardUI).function keywords, JS Automatic Semicolon Insertion (ASI) line-break issues on return statements, and uncalled Date method parentheses.title-label (“TickTock”) out of individual page templates and integrated it directly into the global BaseLayout.astro.<a> tag. Now, clicking the brand logo on any sub-tool instantly routes the user back to the home dashboard Hub, offering an intuitive, web-app-like navigation experience.requestFullscreen() on the individual #clock-screen container caused the browser to isolate it from the DOM layout. This triggered default browser styling which rendered a pitch-black backdrop, and completely hid crucial layout parts like the header, footer, and side menu buttons.document.documentElement (the global <html> node). This aligns the button’s behavior perfectly with standard keyboard F11 fullscreen events.clock.js to digital-clock.js and adjusted the main page path to /src/pages/digital-clock/. This cleanly isolates this feature from upcoming modules like World Clock or Pomodoro Timer./TickTock/ basePath.if(...) conditions inside CSS were breaking colon component constraints.digital-clock.js using the standard document.documentElement.style.setProperty API. The .colon element now seamlessly reads the responsive --clock-colon-width CSS variable, adapting perfectly across Sans, Serif, and Mono configurations without any jumping side effects.#12hr&mono). When you refresh or load the page, your exact preferred theme loads up right away.#date-label placeholder to the template and used the native JavaScript toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) inside the main runtime loop to render lightweight, perfectly formatted timestamps on every update.requestAnimationFrame loop. By mapping high-precision timestamps through Math.sin(), the opacity gracefully curves between 0.2 and 1.0, producing a perfectly smooth, wave-like breathing animation.#clock-screen element, backed by a global fullscreenchange listener to seamlessly catch manual ESC escapes and keep the toggle button states perfectly in sync.<script is:inline> block to the index page. This bypasses the server-side build process and safely binds the sharing and clipboard logic only when the page loads in the user’s browser.src/pages/index.astro) to serve as the gateway for current and future time tools, using a shared layout for a consistent user experience.README.md to document the new project architecture, local testing steps, and automated GitHub Pages deployment.flex-shrink: 0; to the icon class to force them to keep their correct size.repeat(auto-fit, minmax(250px, 1fr)), which should have allowed multiple columns..home-container) was set to display: flex with align-items: center. In a vertical Flexbox layout, this alignment forces child elements like the grid to shrink to their absolute minimum width based on content, instead of filling the screen.max-width: 300px; was hardcoded onto the .tool-grid. Since two cards plus their gaps required at least 525px to sit side-by-side, the 300px restriction made it mathematically impossible for the browser to create a second column.max-width: 300px; from the grid and replaced it with a generous max-width: 1200px; to allow proper breathing room on large screens.width: 100%; to the grid class, forcing the grid container to break out of the Flexbox compression and stretch completely across the parent container.auto-fit engine to finally see the available width and cleanly arrange the cards into multiple columns.box-sizing: border-box; to the main container so that padding is counted inside the total width.width: 100sww; was written instead of 100svw, and added body { margin: 0; } to clear default browser spaces.The project started as a single page where all HTML, CSS, and JS lived together. When I created the unified BaseLayout.astro, I kept the old global.css. However, that file had specific styles just for the countdown page, which broke the layout of the new homepage.
To fix this, I moved the countdown-specific CSS directly into the <style> tag of the countdown page (index.astro). Astro automatically confines these styles to their own page, ensuring they no longer mess up the homepage layout.
I originally placed the logic script (script.js) inside the src/pages/countdown/ folder to keep it close to the page. During local testing, Astro threw an error saying there was no API route handler for that JavaScript file.
I learned that Astro treats every file inside src/pages/ as a public URL or an API route. To fix this, I moved the file to a new src/scripts/ folder and renamed it countdown.js, keeping the pages folder strictly for actual web pages.
Inside my documentation folder, I named a development note file ship#2.md. When I ran npm run build, the entire build process crashed immediately with a path error.
The build tool (Vite) treats the # symbol as a URL anchor link, which breaks file path reading. I renamed the file to ship2.md, and the system was able to parse the path and build successfully.
This was the biggest challenge. When I imported countdown.js into the page, the build failed multiple times, throwing errors like window is not defined and document is not defined.
Astro runs pages on the server to generate static HTML during the build step. Because Node.js does not have browser features like window or document, the script crashed.
I fixed this with two steps:
?url suffix in Astro so the server only records its path instead of running it.typeof window !== "undefined" check. This ensures the script stays quiet during the server build and only runs once it reaches the user’s browser.index.html) and operational state modules (script.js) into standardized Western European typography strings.README.md) specifying setup logic, reactive routing hooks, and custom persistence implementations.#share-menu-btn) inside the header actions block, utilizing the Web Share API to access native mobile distribution panels.loadInitialState) where an premature execution return statement truncated processing flows, preventing state restorations.initTimer), deploying window.history.replaceState() to inject fully compiled variable string parameters back into empty browser URI paths.clamp() function, anchoring layout metrics dynamically between absolute pixel boundaries and relative viewport height units (vh).#event-label) to smoothly scale inside an adaptive range (clamp(5px, 2vh, 30px)), optimizing structural vertical padding on shifting device orientations.#target-label) using fluid scaling boundaries (clamp(15px, 3vh, 40px)), ensuring consistent geometric clear space relative to adjacent countdown blocks.#target-label) from inline formatting definitions into explicit block layout models (display: block), stabilizing custom spacing calculations and forcing uniform horizontal element boundaries.width: 100%) onto trailing description elements, smoothly pushing metadata strings down beneath the dynamic flexible tracking container.localStorage) to preserve client display configurations across session cycles and application reboots.loadInitialState) to safely aggregate user layout states without stepping over parameter strings delivered via URL fragment references.updateTimer) dedicated to managing strict single-unit tracking parameters.font-variant-numeric: tabular-nums to enforce strict monospaced numeral allocation, keeping external box dimensions static regardless of structural text mutations..mode-single) to adjust component paddings, implement generous baseline tracking, and elevate text sizing bounds when processing simplified metrics.style.css and script.js internal resources.index.html structure down to essential structural markup definitions, using external linkages to boost source reading efficiency and code maintainability.ReferenceError inside the titleInput listener by swapping an un-sanitized token (newDate) with the valid, persistent variable targetString.if(newTitle="")) muted intended comparison logic, replacing it with a strict equality check (newTitle === "").decodeURIComponent() into loadInitialState() and the hashchange observer to prevent percent-encoded character corruption.<meta name="viewport" content="width=device-width, initial-scale=1.0"> into the document structure header.@media (max-width: 600px) breakpoint directives, downscaling internal flex container gaps from 15px to 10px and adjusting modular countdown component nodes for enhanced display symmetry./^\d{4}-\d{2}-\d{2}$/) to sanitize incoming window location string tokens prior to committing them to the execution state.initTimer(). This routine synchronously updates text container parameters, binds default input values, clears residual interval clocks, and re-allocates tracking loops.initTimer() wrapper where repeating the let primitive on the timerId reference allocated a local block-scope variable that collided with pre-initialization clear routines. Removing the redundant declarative keyword reinstated proper execution.initTimer(targetString) to successfully boot up active countdown cycles on fresh page load sequences.<button> containing a <span> with the material-icons class and the text “menu” to render the hamburger icon.background: none, border: none) to remove default browser styling and positioned it at the top-left corner using position: absolute.Hide time boxes that have a value of 0 to keep the UI clean, but prevent a box from hiding if a larger time unit still holds a value (e.g., if years > 0, months must not be hidden even if months == 0).
Implemented a cascading flag logic (let hasValue = false) executed from the largest unit (years) down to the smallest (minutes). Once any larger unit turns the flag true, all subsequent boxes remain visible regardless of their own value. Box elements are hidden dynamically using parentElement.style.display = "none".
The previous countdown relied on dividing total seconds, which failed to calculate years and months accurately because real-world months have a variable number of days (28, 30, 31).
Shifted from pure division to a calendar-based object subtraction method. The system now calculates the differences for Y, M, D, H, M, S individually and applies a borrowing logic from the smallest unit up to the largest. It uses new Date(Y, M, 0).getDate() to dynamically fetch the exact number of days in the previous month when borrowing days.
The correct condition is mo < 0. The variable represents the difference between months. If mo equals 0, the target month and current month are the same, meaning no year needs to be borrowed. Using <= 0 would trigger a false borrow when months are identical, causing the month value to incorrectly jump by +12 and the year to decrease by 1.
Corrected the day borrowing condition from if (mo < 0) to if (d < 0).
Fixed a typo where let m (minutes) was mistakenly subtracting now.getMonth() instead of now.getMinutes().
add some todos and changelog
the first project using html this year
making a countdown timer