From 6b9ef39908b0ef990e0473469a7f671bd8c5171b Mon Sep 17 00:00:00 2001 From: spc-agent-00 Date: Mon, 27 Jul 2026 08:39:45 +0000 Subject: [PATCH] feat: build responsive time tracker interface --- time/index.html | 4 +- time/src/App.css | 988 +++++++++++++++++++++++++++++++++++++++------ time/src/App.tsx | 345 ++++++++++++++-- time/src/index.css | 133 +++--- 4 files changed, 1218 insertions(+), 252 deletions(-) diff --git a/time/index.html b/time/index.html index 3004327..3702636 100644 --- a/time/index.html +++ b/time/index.html @@ -4,7 +4,9 @@ - time + + + Design Time Tracker
diff --git a/time/src/App.css b/time/src/App.css index f90339d..08427f5 100644 --- a/time/src/App.css +++ b/time/src/App.css @@ -1,184 +1,906 @@ -.counter { +.page { + --project-color: #16a36a; + --project-tint: #e9f8f0; + min-height: 100vh; + padding: 32px 24px; + background: + radial-gradient(circle at 12% 0%, color-mix(in srgb, var(--project-color) 8%, transparent), transparent 28rem), + var(--background); +} + +.tracker { + width: min(100%, 1080px); + margin: 0 auto; + overflow: hidden; + background: var(--card); + border: 1px solid color-mix(in srgb, var(--border) 92%, #000000); + border-radius: 24px; + box-shadow: 0 22px 60px rgba(30, 45, 39, 0.08), 0 2px 8px rgba(30, 45, 39, 0.04); +} + +.tracker-header { + position: relative; + padding: 28px 32px 26px; + overflow: hidden; + background: + linear-gradient(90deg, color-mix(in srgb, var(--project-color) 5%, transparent) 1px, transparent 1px), + linear-gradient(color-mix(in srgb, var(--project-color) 5%, transparent) 1px, transparent 1px), + linear-gradient(135deg, #ffffff 0%, var(--project-tint) 100%); + background-size: 22px 22px, 22px 22px, auto; + border-bottom: 1px solid color-mix(in srgb, var(--project-color) 24%, var(--border)); +} + +.tracker-header::after { + position: absolute; + top: -82px; + right: -54px; + width: 250px; + height: 250px; + content: ''; + pointer-events: none; + background: var(--project-color); + border-radius: 50%; + opacity: 0.055; +} + +.header-topline, +.session-controls, +.toolbar, +.day-heading, +.tracker-footer, +.entry-title-row, +.entry-footer, +.active-title { + display: flex; + align-items: center; +} + +.header-topline { + position: relative; + z-index: 1; + justify-content: space-between; + gap: 24px; + margin-bottom: 26px; +} + +.brand { + display: flex; + align-items: center; + min-width: 0; + gap: 13px; +} + +.brand-mark { + display: grid; + flex: 0 0 auto; + width: 42px; + height: 42px; + color: #ffffff; + background: var(--project-color); + border-radius: 13px; + box-shadow: 0 7px 18px color-mix(in srgb, var(--project-color) 28%, transparent); + place-items: center; +} + +.brand h1 { + margin: 0; + color: var(--card-foreground); + font-size: 21px; + font-weight: 700; + letter-spacing: -0.45px; + line-height: 1.2; +} + +.brand p { + margin: 4px 0 0; + color: var(--muted-foreground); + font-size: 13px; +} + +.tracking-status { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + min-height: 30px; + padding: 0 11px; + color: color-mix(in srgb, var(--project-color) 88%, #0e3325); + background: color-mix(in srgb, var(--project-tint) 80%, #ffffff); + border: 1px solid color-mix(in srgb, var(--project-color) 25%, transparent); + border-radius: 999px; + font-size: 12px; + font-weight: 700; + gap: 7px; +} + +.status-dot, +.entry-live-dot { + width: 7px; + height: 7px; + background: #a8b1ad; + border-radius: 50%; +} + +.status-dot.is-live, +.entry-live-dot { + background: var(--project-color); + box-shadow: 0 0 0 4px color-mix(in srgb, var(--project-color) 15%, transparent); +} + +.status-dot.is-live { + animation: soft-pulse 2s ease-out infinite; +} + +@keyframes soft-pulse { + 0%, 100% { box-shadow: 0 0 0 3px color-mix(in srgb, var(--project-color) 14%, transparent); } + 50% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--project-color) 5%, transparent); } +} + +.session-controls { + position: relative; + z-index: 1; + display: grid; + grid-template-columns: minmax(180px, 1fr) minmax(180px, 1.25fr) 148px 50px; + gap: 14px; + align-items: end; +} + +.field { + display: flex; + min-width: 0; + flex-direction: column; + align-items: stretch; + gap: 7px; +} + +.field > span:first-child { + color: #66706c; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +.select-wrap, +.tag-value, +.timer-field strong { + height: 44px; + background: rgba(255, 255, 255, 0.84); + border: 1px solid color-mix(in srgb, var(--project-color) 15%, var(--input)); + border-radius: 10px; + box-shadow: 0 1px 2px rgba(25, 45, 36, 0.03); +} + +.select-wrap { + position: relative; + display: flex; + align-items: center; +} + +.project-swatch { + position: absolute; + z-index: 1; + left: 13px; + width: 8px; + height: 8px; + pointer-events: none; + background: var(--project-color); + border-radius: 3px; +} + +.select-wrap::after { + position: absolute; + right: 14px; + width: 7px; + height: 7px; + content: ''; + pointer-events: none; + border-right: 1.5px solid #75807b; + border-bottom: 1.5px solid #75807b; + transform: translateY(-2px) rotate(45deg); +} + +.select-wrap select { + width: 100%; + height: 100%; + padding: 0 35px 0 31px; + color: var(--foreground); + appearance: none; + background: transparent; + border: 0; + font-size: 14px; + font-weight: 600; + cursor: pointer; +} + +.tag-value { + display: flex; + align-items: center; + padding: 0 13px; + color: #46514d; + border-style: solid; + font-size: 14px; + font-weight: 600; + gap: 8px; + cursor: pointer; +} + +.tag-value .icon { + color: var(--project-color); +} + +.timer-field strong { + display: flex; + align-items: center; + justify-content: center; + color: #24302c; font-size: 16px; - padding: 5px 10px; + font-variant-numeric: tabular-nums; + letter-spacing: 0.055em; +} + +.timer-toggle { + display: grid; + width: 50px; + height: 44px; + padding: 0; + color: #ffffff; + background: var(--project-color); + border: 0; + border-radius: 12px; + box-shadow: 0 6px 16px color-mix(in srgb, var(--project-color) 25%, transparent); + cursor: pointer; + transition: transform 160ms ease, box-shadow 160ms ease; + place-items: center; +} + +.timer-toggle.is-tracking { + color: #ef5a63; + background: #ffffff; + border: 1px solid color-mix(in srgb, #ef5a63 30%, transparent); + box-shadow: 0 5px 16px rgba(239, 90, 99, 0.13); +} + +.timer-toggle:hover { + box-shadow: 0 8px 22px color-mix(in srgb, var(--project-color) 28%, transparent); + transform: translateY(-1px); +} + +.timer-toggle.is-tracking:hover { + box-shadow: 0 8px 22px rgba(239, 90, 99, 0.18); +} + +.toolbar { + min-height: 66px; + justify-content: space-between; + padding: 10px 32px; + background: #ffffff; + border-bottom: 1px solid var(--border); + gap: 20px; +} + +.date-selector, +.interval-control { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.icon-button { + display: grid; + width: 34px; + height: 34px; + padding: 0; + color: #68726e; + background: transparent; + border: 0; + border-radius: 9px; + cursor: pointer; + transition: background 150ms ease, color 150ms ease; + place-items: center; +} + +.icon-button:hover:not(:disabled) { + color: var(--foreground); + background: var(--secondary); +} + +.icon-button:disabled { + cursor: not-allowed; + opacity: 0.35; +} + +.date-button { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 146px; + height: 38px; + padding: 0 15px; + color: #28342f; + background: #ffffff; + border: 1px solid var(--input); + border-radius: 10px; + box-shadow: 0 1px 2px rgba(20, 35, 29, 0.04); + font-size: 14px; + font-weight: 700; + gap: 8px; + cursor: pointer; +} + +.date-button .icon { + color: var(--project-color); +} + +.interval-control { + min-width: 210px; + justify-content: flex-end; +} + +.interval-label { + margin-right: 6px; + color: var(--muted-foreground); + font-size: 12px; + font-weight: 600; +} + +.interval-control strong { + min-width: 52px; + color: #303b37; + font-size: 13px; + font-variant-numeric: tabular-nums; + text-align: center; +} + +.day-heading { + justify-content: space-between; + padding: 27px 32px 22px 84px; +} + +.day-heading p, +.daily-total span { + margin: 0 0 5px; + color: var(--muted-foreground); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.055em; + text-transform: uppercase; +} + +.day-heading h2 { + margin: 0; + color: var(--foreground); + font-size: 19px; + font-weight: 700; + letter-spacing: -0.3px; +} + +.daily-total { + text-align: right; +} + +.daily-total span { + display: block; +} + +.daily-total strong { + color: #26322e; + font-size: 17px; + font-variant-numeric: tabular-nums; +} + +.timeline { + display: grid; + grid-template-columns: 68px minmax(0, 1fr); + min-height: 650px; + padding: 0 32px 0 16px; +} + +.time-axis, +.timeline-canvas { + display: grid; + grid-template-rows: repeat(10, 1fr) 1px; +} + +.time-axis { + padding-right: 14px; +} + +.time-axis span { + color: #9aa19e; + font-size: 10px; + font-weight: 600; + line-height: 1; + text-align: right; + transform: translateY(-4px); +} + +.timeline-canvas { + position: relative; + min-width: 0; + border-left: 1px solid #edf0ee; +} + +.hour-line { + border-top: 1px solid #edf0ee; +} + +.hour-line:last-of-type { + border-bottom: 1px solid #edf0ee; +} + +.time-entry { + position: absolute; + z-index: 2; + display: flex; + left: 18px; + width: min(68%, 590px); + min-width: 320px; + overflow: hidden; + background: #ffffff; + border: 1px solid; + border-radius: 12px; + box-shadow: 0 7px 18px rgba(31, 47, 40, 0.07); + transition: box-shadow 160ms ease, transform 160ms ease; +} + +.time-entry:hover { + box-shadow: 0 11px 24px rgba(31, 47, 40, 0.11); + transform: translateY(-1px); +} + +.entry-accent { + flex: 0 0 4px; + background: currentColor; +} + +.entry-copy { + width: 100%; + min-width: 0; + padding: 12px 14px 11px; +} + +.entry-title-row { + justify-content: space-between; + min-width: 0; + gap: 16px; +} + +.entry-title-row h3 { + overflow: hidden; + margin: 0; + font-size: 14px; + font-weight: 700; + letter-spacing: -0.1px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.entry-title-row > strong { + flex: 0 0 auto; + color: #36423d; + font-size: 13px; + font-variant-numeric: tabular-nums; + letter-spacing: 0.03em; +} + +.entry-copy > p { + margin: 4px 0 0; + color: #69736f; + font-size: 11px; +} + +.entry-range { + color: #7e8783; + font-size: 10px; + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.other-entry { + top: 8%; + height: 12%; + min-height: 70px; + color: #e84d58; + background: #fff8f8; + border-color: #f6c7ca; +} + +.other-entry .entry-range { + position: absolute; + right: 14px; + bottom: 10px; +} + +.active-entry { + top: 29%; + height: 18%; + min-height: 94px; + color: var(--project-color); + background: + linear-gradient(45deg, color-mix(in srgb, var(--project-color) 4%, transparent) 25%, transparent 25%, transparent 75%, color-mix(in srgb, var(--project-color) 4%, transparent) 75%), + linear-gradient(45deg, color-mix(in srgb, var(--project-color) 4%, transparent) 25%, transparent 25%, transparent 75%, color-mix(in srgb, var(--project-color) 4%, transparent) 75%), + color-mix(in srgb, var(--project-tint) 80%, #ffffff); + background-position: 0 0, 4px 4px; + background-size: 8px 8px; + border-color: color-mix(in srgb, var(--project-color) 50%, #ffffff); +} + +.active-entry.is-running { + box-shadow: 0 8px 22px color-mix(in srgb, var(--project-color) 14%, transparent); +} + +.active-title { + min-width: 0; + gap: 9px; +} + +.entry-live-dot { + flex: 0 0 auto; + width: 6px; + height: 6px; +} + +.entry-footer { + position: absolute; + right: 14px; + bottom: 10px; + left: 18px; + justify-content: space-between; + gap: 12px; +} + +.tag-chip { + display: inline-flex; + align-items: center; + height: 20px; + padding: 0 8px; + color: color-mix(in srgb, var(--project-color) 86%, #16382b); + background: color-mix(in srgb, var(--project-color) 10%, #ffffff); + border: 1px solid color-mix(in srgb, var(--project-color) 16%, transparent); + border-radius: 999px; + font-size: 9px; + font-weight: 700; +} + +.now-line { + position: absolute; + z-index: 4; + right: 0; + left: -1px; + display: flex; + align-items: center; + pointer-events: none; + transform: translateY(-50%); +} + +.now-line span { + display: grid; + flex: 0 0 auto; + height: 21px; + padding: 0 7px; + color: #ffffff; + background: #26332e; border-radius: 5px; - color: var(--accent); - background: var(--accent-bg); - border: 2px solid transparent; - transition: border-color 0.3s; - margin-bottom: 24px; + font-size: 9px; + font-weight: 700; + place-items: center; +} + +.now-line i { + width: 100%; + border-top: 1px solid #53605b; +} + +.now-line i::after { + position: absolute; + top: 50%; + right: -2px; + width: 5px; + height: 5px; + content: ''; + background: #53605b; + border-radius: 50%; + transform: translateY(-50%); +} + +.tracker-footer { + min-height: 68px; + margin-top: 24px; + padding: 12px 32px; + color: #7b8581; + background: #fafbfa; + border-top: 1px solid var(--border); + font-size: 11px; + font-weight: 600; + gap: 22px; +} + +.tracker-footer > span { + display: inline-flex; + align-items: center; + gap: 7px; +} + +.legend-dot { + display: inline-block; + width: 7px; + height: 7px; + border-radius: 50%; +} + +.legend-dot.active { + background: var(--project-color); +} + +.legend-dot.saved { + background: #ef5a63; +} - &:hover { - border-color: var(--accent-border); +.tracker-footer button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 34px; + margin-left: auto; + padding: 0 12px; + color: #394540; + background: #ffffff; + border: 1px solid var(--border); + border-radius: 9px; + font-size: 11px; + font-weight: 700; + gap: 7px; + cursor: pointer; +} + +.tracker-footer button:hover { + color: var(--project-color); + border-color: color-mix(in srgb, var(--project-color) 24%, var(--border)); +} + +@media (max-width: 760px) { + .page { + padding: 0; } - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; + + .tracker { + min-height: 100vh; + border: 0; + border-radius: 0; + box-shadow: none; } -} -.hero { - position: relative; + .tracker-header { + padding: 22px 20px 20px; + } - .base, - .framework, - .vite { - inset-inline: 0; - margin: 0 auto; + .header-topline { + margin-bottom: 22px; } - .base { - width: 170px; - position: relative; - z-index: 0; + .brand-mark { + width: 38px; + height: 38px; + border-radius: 11px; } - .framework, - .vite { - position: absolute; + .brand h1 { + font-size: 18px; } - .framework { - z-index: 1; - top: 34px; - height: 28px; - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) - scale(1.4); + .brand p { + display: none; } - .vite { - z-index: 0; - top: 107px; - height: 26px; - width: auto; - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) - scale(0.8); + .tracking-status { + min-height: 27px; + padding: 0 9px; + font-size: 10px; } -} -#center { - display: flex; - flex-direction: column; - gap: 25px; - place-content: center; - place-items: center; - flex-grow: 1; + .session-controls { + grid-template-columns: minmax(0, 1fr) minmax(104px, .65fr) 46px; + gap: 9px; + } - @media (max-width: 1024px) { - padding: 32px 20px 24px; - gap: 18px; + .tags-field { + display: none; } -} -#next-steps { - display: flex; - border-top: 1px solid var(--border); - text-align: left; + .timer-field strong, + .select-wrap, + .timer-toggle { + height: 42px; + } - & > div { - flex: 1 1 0; - padding: 32px; - @media (max-width: 1024px) { - padding: 24px 20px; - } + .timer-field strong { + padding: 0 8px; + font-size: 13px; } - .icon { - margin-bottom: 16px; - width: 22px; - height: 22px; + .timer-toggle { + width: 46px; } - @media (max-width: 1024px) { - flex-direction: column; - text-align: center; + .field > span:first-child { + font-size: 9px; } -} -#docs { - border-right: 1px solid var(--border); + .toolbar { + min-height: 60px; + padding: 8px 14px; + } - @media (max-width: 1024px) { - border-right: none; - border-bottom: 1px solid var(--border); + .interval-control { + min-width: auto; } -} -#next-steps ul { - list-style: none; - padding: 0; - display: flex; - gap: 8px; - margin: 32px 0 0; + .interval-label { + display: none; + } - .logo { - height: 18px; + .interval-control strong { + min-width: 43px; + font-size: 11px; } - a { - color: var(--text-h); + .date-button { + min-width: 104px; + height: 36px; + padding: 0 10px; + font-size: 12px; + } + + .date-button .icon { + display: none; + } + + .icon-button { + width: 30px; + height: 32px; + } + + .day-heading { + padding: 23px 17px 20px 63px; + } + + .day-heading h2 { font-size: 16px; - border-radius: 6px; - background: var(--social-bg); - display: flex; - padding: 6px 12px; - align-items: center; + } + + .daily-total strong { + font-size: 15px; + } + + .timeline { + grid-template-columns: 52px minmax(0, 1fr); + min-height: 590px; + padding: 0 16px 0 5px; + } + + .time-axis { + padding-right: 10px; + } + + .time-axis span { + font-size: 9px; + } + + .time-entry { + left: 10px; + width: calc(100% - 18px); + min-width: 0; + border-radius: 10px; + } + + .entry-copy { + padding: 10px 11px; + } + + .entry-title-row { gap: 8px; - text-decoration: none; - transition: box-shadow 0.3s; + } + + .entry-title-row h3 { + font-size: 12px; + } - &:hover { - box-shadow: var(--shadow); - } - .button-icon { - height: 18px; - width: 18px; - } + .entry-title-row > strong { + font-size: 11px; } - @media (max-width: 1024px) { - margin-top: 20px; - flex-wrap: wrap; - justify-content: center; + .entry-copy > p { + font-size: 10px; + } - li { - flex: 1 1 calc(50% - 8px); - } + .entry-footer { + right: 11px; + bottom: 9px; + left: 15px; + } - a { - width: 100%; - justify-content: center; - box-sizing: border-box; - } + .other-entry .entry-range { + right: 11px; + bottom: 8px; } -} -#spacer { - height: 88px; - border-top: 1px solid var(--border); - @media (max-width: 1024px) { - height: 48px; + .tracker-footer { + min-height: 62px; + margin-top: 18px; + padding: 10px 16px; + gap: 14px; + } + + .tracker-footer > span:nth-child(2) { + display: none; } } -.ticks { - position: relative; - width: 100%; +@media (max-width: 420px) { + .tracker-header { + padding-right: 14px; + padding-left: 14px; + } - &::before, - &::after { - content: ''; - position: absolute; - top: -4.5px; - border: 5px solid transparent; + .brand { + gap: 9px; } - &::before { - left: 0; - border-left-color: var(--border); + .brand h1 { + font-size: 16px; + } + + .brand-mark { + width: 34px; + height: 34px; + } + + .session-controls { + grid-template-columns: minmax(0, 1fr) auto; + } + + .timer-field { + grid-row: 2; + grid-column: 1 / -1; } - &::after { - right: 0; - border-right-color: var(--border); + + .timer-field strong { + justify-content: flex-start; + padding-left: 13px; + } + + .timer-toggle { + grid-row: 1; + grid-column: 2; + } + + .toolbar { + gap: 5px; + } + + .date-selector, + .interval-control { + gap: 0; + } + + .date-button { + min-width: 88px; + } + + .day-heading { + padding-left: 54px; + } + + .day-heading p, + .daily-total span { + font-size: 9px; + } + + .day-heading h2 { + font-size: 14px; + } + + .tracker-footer button { + padding: 0 9px; } } diff --git a/time/src/App.tsx b/time/src/App.tsx index 30c3f29..d995412 100644 --- a/time/src/App.tsx +++ b/time/src/App.tsx @@ -1,37 +1,322 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' +import { useEffect, useMemo, useState, type CSSProperties, type ReactNode } from 'react' import './App.css' -import { FrappeProvider } from 'frappe-react-sdk' + +type IconName = + | 'calendar' + | 'chevron-left' + | 'chevron-right' + | 'clock' + | 'minus' + | 'pause' + | 'play' + | 'plus' + | 'square' + | 'tag' + +type Project = { + id: string + name: string + color: string + tint: string +} + +const projects: Project[] = [ + { id: 'pi3ch', name: 'pi3ch', color: '#16a36a', tint: '#e9f8f0' }, + { id: 'northstar', name: 'Northstar', color: '#7c5ce5', tint: '#f0edff' }, + { id: 'other', name: 'Other Project', color: '#ef5a63', tint: '#fff0f1' }, +] + +const intervalOptions = [5, 10, 15, 30, 60, 120] +const hourLabels = Array.from({ length: 11 }, (_, index) => index + 8) + +function Icon({ name, size = 18 }: { name: IconName; size?: number }) { + let content: ReactNode + + switch (name) { + case 'calendar': + content = <> + break + case 'chevron-left': + content = + break + case 'chevron-right': + content = + break + case 'clock': + content = <> + break + case 'minus': + content = + break + case 'pause': + content = <> + break + case 'play': + content = + break + case 'plus': + content = + break + case 'square': + content = + break + case 'tag': + content = <> + break + } + + return ( + + ) +} + +function formatClock(totalSeconds: number) { + const hours = Math.floor(totalSeconds / 3600) + const minutes = Math.floor((totalSeconds % 3600) / 60) + const seconds = totalSeconds % 60 + return [hours, minutes, seconds].map((part) => String(part).padStart(2, '0')).join(':') +} + +function isSameDay(first: Date, second: Date) { + return first.getFullYear() === second.getFullYear() + && first.getMonth() === second.getMonth() + && first.getDate() === second.getDate() +} + +function dateLabel(date: Date) { + if (isSameDay(date, new Date())) return 'Today' + return new Intl.DateTimeFormat('en', { + weekday: 'short', + month: 'short', + day: 'numeric', + }).format(date) +} + +function fullDateLabel(date: Date) { + return new Intl.DateTimeFormat('en', { + weekday: 'long', + month: 'long', + day: 'numeric', + }).format(date) +} + function App() { - const [count, setCount] = useState(0) + const [projectId, setProjectId] = useState(projects[0].id) + const [selectedDate, setSelectedDate] = useState(() => new Date()) + const [intervalMinutes, setIntervalMinutes] = useState(15) + const [elapsedSeconds, setElapsedSeconds] = useState(37 * 60 + 12) + const [isTracking, setIsTracking] = useState(true) + const [now, setNow] = useState(() => new Date()) + + const project = projects.find((item) => item.id === projectId) ?? projects[0] + const selectedIntervalIndex = intervalOptions.indexOf(intervalMinutes) + + useEffect(() => { + const clock = window.setInterval(() => setNow(new Date()), 30_000) + return () => window.clearInterval(clock) + }, []) + + useEffect(() => { + if (!isTracking) return + const timer = window.setInterval(() => setElapsedSeconds((value) => value + 1), 1_000) + return () => window.clearInterval(timer) + }, [isTracking]) + + const todayTotal = useMemo(() => elapsedSeconds + (54 * 60) + (42 * 60), [elapsedSeconds]) + + function moveDate(dayDelta: number) { + setSelectedDate((current) => { + const next = new Date(current) + next.setDate(current.getDate() + dayDelta) + return next + }) + } + + function changeInterval(direction: -1 | 1) { + const nextIndex = Math.min( + intervalOptions.length - 1, + Math.max(0, selectedIntervalIndex + direction), + ) + setIntervalMinutes(intervalOptions[nextIndex]) + } + + const nowPosition = Math.min(100, Math.max(0, ((now.getHours() * 60 + now.getMinutes()) - 480) / 600 * 100)) + const projectStyle = { + '--project-color': project.color, + '--project-tint': project.tint, + } as CSSProperties return ( -
- -
- -

Vite + React + Frappe

-
- -

- Edit src/App.jsx and save to test HMR -

-
-

- Click on the Vite and React logos to learn more -

-
-
-
+
+
+
+
+
+ +
+

Design Time Tracker

+

Focus session in progress

+
+
+ +
+ + {isTracking ? 'Tracking' : 'Paused'} +
+
+ +
+ + +
+ Tags + +
+ +
+ Current session + {formatClock(elapsedSeconds)} +
+ + +
+
+ +
+
+ + + +
+ +
+ Snap + + {intervalMinutes < 60 ? `${intervalMinutes} min` : `${intervalMinutes / 60} hr`} + +
+
+ +
+
+

{isSameDay(selectedDate, new Date()) ? 'Your day' : 'Daily timeline'}

+

{fullDateLabel(selectedDate)}

+
+
+ Total tracked + {formatClock(todayTotal).slice(0, 5)} +
+
+ +
+ + +
+ {hourLabels.map((hour) =>
)} + +
+
+
+
+

Other Project

+ 00:54:00 +
+

Design Time Tracker

+ 09:05 – 09:59 +
+
+ +
+
+
+
+
+ +

{project.name}

+
+ {formatClock(elapsedSeconds)} +
+

Design Time Tracker

+
+ Design + 10:05 – {isTracking ? 'Now' : '10:42'} +
+
+
+ + {isSameDay(selectedDate, new Date()) && ( +
+ Now + +
+ )} +
+
+ +
+ Active session + Saved entry + +
+
+
) } diff --git a/time/src/index.css b/time/src/index.css index 5fb3313..09918bf 100644 --- a/time/src/index.css +++ b/time/src/index.css @@ -1,111 +1,68 @@ -:root { - --text: #6b6375; - --text-h: #08060d; - --bg: #fff; - --border: #e5e4e7; - --code-bg: #f4f3ec; - --accent: #aa3bff; - --accent-bg: rgba(170, 59, 255, 0.1); - --accent-border: rgba(170, 59, 255, 0.5); - --social-bg: rgba(244, 243, 236, 0.5); - --shadow: - rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px; +@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap'); - --sans: system-ui, 'Segoe UI', Roboto, sans-serif; - --heading: system-ui, 'Segoe UI', Roboto, sans-serif; - --mono: ui-monospace, Consolas, monospace; +:root { + --background: #f5f6f8; + --foreground: #17201d; + --card: #ffffff; + --card-foreground: #17201d; + --popover: #ffffff; + --popover-foreground: #17201d; + --primary: #17201d; + --primary-foreground: #ffffff; + --secondary: #f1f3f2; + --secondary-foreground: #26322e; + --muted: #f5f7f6; + --muted-foreground: #78817e; + --accent: #eef7f2; + --accent-foreground: #137a51; + --destructive: #ef5a63; + --destructive-foreground: #ffffff; + --border: #e4e8e6; + --input: #dfe4e2; + --ring: #19a76d; + --radius: 0.75rem; + --font-sans: 'DM Sans', Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; - color: var(--text); - background: var(--bg); + color: var(--foreground); + background: var(--background); + font-family: var(--font-sans); font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - - @media (max-width: 1024px) { - font-size: 16px; - } -} - -@media (prefers-color-scheme: dark) { - :root { - --text: #9ca3af; - --text-h: #f3f4f6; - --bg: #16171d; - --border: #2e303a; - --code-bg: #1f2028; - --accent: #c084fc; - --accent-bg: rgba(192, 132, 252, 0.15); - --accent-border: rgba(192, 132, 252, 0.5); - --social-bg: rgba(47, 48, 58, 0.5); - --shadow: - rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px; - } - - #social .button-icon { - filter: invert(1) brightness(2); - } } -#root { - width: 1126px; - max-width: 100%; - margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); - min-height: 100svh; - display: flex; - flex-direction: column; +* { box-sizing: border-box; } body { + min-width: 320px; + min-height: 100vh; margin: 0; } -h1, -h2 { - font-family: var(--heading); - font-weight: 500; - color: var(--text-h); +button, +select { + font: inherit; } -h1 { - font-size: 56px; - letter-spacing: -1.68px; - margin: 32px 0; - @media (max-width: 1024px) { - font-size: 36px; - margin: 20px 0; - } -} -h2 { - font-size: 24px; - line-height: 118%; - letter-spacing: -0.24px; - margin: 0 0 8px; - @media (max-width: 1024px) { - font-size: 20px; - } +button, +select, +.time-entry { + -webkit-tap-highlight-color: transparent; } -p { - margin: 0; + +button:focus-visible, +select:focus-visible { + outline: 3px solid color-mix(in srgb, var(--ring) 24%, transparent); + outline-offset: 2px; } -code, -.counter { - font-family: var(--mono); - display: inline-flex; - border-radius: 4px; - color: var(--text-h); +button { + color: inherit; } -code { - font-size: 15px; - line-height: 135%; - padding: 4px 8px; - background: var(--code-bg); +#root { + min-height: 100vh; }