also looking at this
feat(dashboard): presets-and-calendar reporting-period picker
#55
3 files
+457
-34
| @@ -54,6 +54,15 @@ let from = isAll | ||
| 54 | 54 | : isCustom ? `${customFrom}T00:00:00.000Z` : new Date(Date.now() - days * 864e5).toISOString() |
| 55 | 55 | if (isCustom) |
| 56 | 56 | days = Math.max(1, Math.round((new Date(to).getTime() - new Date(from).getTime()) / 864e5)) |
| 57 | // What the period control reads. A preset keeps its name and the days it resolves | |
| 58 | // to; 'all' has no start until loadData finds the earliest pageview, so it is | |
| 59 | // named alone. Dates are formatted in UTC because from/to are UTC day bounds. | |
| 60 | const RANGE_LABELS = { '24h': 'Last 24 hours', '7d': 'Last 7 days', '30d': 'Last 30 days', '90d': 'Last 90 days', '1y': 'Last 365 days', 'all': 'All time' } | |
| 61 | const fmtDay = s => new Date(`${s.slice(0, 10)}T00:00:00Z`).toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' }) | |
| 62 | const fmtSpan = (a, b) => a.slice(0, 4) === b.slice(0, 4) | |
| 63 | ? `${fmtDay(a)} – ${fmtDay(b)}, ${b.slice(0, 4)}` | |
| 64 | : `${fmtDay(a)}, ${a.slice(0, 4)} – ${fmtDay(b)}, ${b.slice(0, 4)}` | |
| 65 | const rangeLabel = isCustom ? fmtSpan(customFrom, customTo) : isAll ? 'All time' : `${RANGE_LABELS[range] ?? 'Custom'} · ${fmtSpan(from, to)}` | |
| 57 | 66 | // Chart bucket granularity by span: hour (<= 2d) for a real 24h curve, day (<= ~400d), |
| 58 | 67 | // or month (wider, e.g. all-time) so a multi-year chart isn't thousands of points. |
| 59 | 68 | // bucketLen = the LEFT(timestamp, N) prefix — 'YYYY-MM-DDTHH'(13) / 'YYYY-MM-DD'(10) / |
| @@ -2169,30 +2178,79 @@ summary:focus-visible { | ||
| 2169 | 2178 | right: 0; |
| 2170 | 2179 | top: calc(100% + 0.5rem); |
| 2171 | 2180 | z-index: 40; |
| 2172 | width: min(24rem, calc(100vw - 2.5rem)); | |
| 2173 | padding: 1rem; | |
| 2181 | width: min(44rem, calc(100vw - 2.5rem)); | |
| 2182 | padding: 0.75rem; | |
| 2174 | 2183 | border: 1px solid var(--border); |
| 2175 | 2184 | border-radius: 12px; |
| 2176 | 2185 | background: var(--panel); |
| 2177 | 2186 | box-shadow: 0 18px 50px color-mix(in srgb, var(--text) 10%, transparent); |
| 2178 | 2187 | } |
| 2179 | .date-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; } | |
| 2180 | .date-field label { | |
| 2188 | /* Period picker: presets down the left, two months on the right. The range is | |
| 2189 | drawn flat with rounded ends, the way Fathom's is, on the --bar wash the KPI | |
| 2190 | bars already use so it reads as the same accent at the same strength. */ | |
| 2191 | .drp { display: grid; grid-template-columns: 10.5rem minmax(0, 1fr); gap: 0.75rem; } | |
| 2192 | .drp-presets { display: flex; flex-direction: column; gap: 0.15rem; padding-right: 0.75rem; border-right: 1px solid var(--border); } | |
| 2193 | .drp-preset { | |
| 2181 | 2194 | display: block; |
| 2182 | margin-bottom: 0.35rem; | |
| 2195 | width: 100%; | |
| 2196 | padding: 0.5rem 0.7rem; | |
| 2197 | border: 0; | |
| 2198 | border-radius: 8px; | |
| 2199 | background: transparent; | |
| 2200 | color: var(--text-2); | |
| 2201 | font: 500 0.8125rem/1.2 var(--sans, ui-sans-serif); | |
| 2202 | text-align: left; | |
| 2203 | cursor: pointer; | |
| 2204 | } | |
| 2205 | .drp-preset:hover { color: var(--text); background: color-mix(in srgb, var(--text) 6%, transparent); } | |
| 2206 | .drp-preset.is-active { color: var(--accent-ink); background: var(--accent); } | |
| 2207 | .drp-cals { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; } | |
| 2208 | .drp-head { display: grid; grid-template-columns: 2rem minmax(0, 1fr) minmax(0, 1fr) 2rem; align-items: center; gap: 0.35rem; margin-bottom: 0.5rem; } | |
| 2209 | .drp-nav { | |
| 2210 | display: inline-grid; | |
| 2211 | place-items: center; | |
| 2212 | width: 2rem; | |
| 2213 | height: 2rem; | |
| 2214 | padding: 0; | |
| 2215 | border: 1px solid transparent; | |
| 2216 | border-radius: 8px; | |
| 2217 | background: transparent; | |
| 2183 | 2218 | color: var(--text-2); |
| 2184 | font: 600 0.75rem/1.2 var(--sans, ui-sans-serif); | |
| 2219 | cursor: pointer; | |
| 2185 | 2220 | } |
| 2186 | .date-field input { | |
| 2187 | width: 100%; | |
| 2188 | padding: 0.55rem 0.65rem; | |
| 2221 | .drp-nav:hover { color: var(--text); border-color: var(--border); } | |
| 2222 | .drp-nav svg { width: 1rem; height: 1rem; } | |
| 2223 | .drp-nav.is-hidden { visibility: hidden; } | |
| 2224 | .drp-sel { | |
| 2225 | min-width: 0; | |
| 2226 | height: 2rem; | |
| 2227 | padding: 0 0.5rem; | |
| 2189 | 2228 | border: 1px solid var(--border); |
| 2190 | 2229 | border-radius: 8px; |
| 2191 | 2230 | background: var(--bg); |
| 2192 | 2231 | color: var(--text); |
| 2232 | font: 600 0.8125rem/1 var(--sans, ui-sans-serif); | |
| 2233 | } | |
| 2234 | .drp-dow, .drp-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); } | |
| 2235 | .drp-dow span { padding: 0.25rem 0; color: var(--text-3); font: 600 0.7rem/1.2 var(--sans, ui-sans-serif); text-align: center; } | |
| 2236 | .drp-day { | |
| 2237 | height: 2.25rem; | |
| 2238 | padding: 0; | |
| 2239 | border: 0; | |
| 2240 | border-radius: 0; | |
| 2241 | background: transparent; | |
| 2242 | color: var(--text); | |
| 2193 | 2243 | font: 500 0.8125rem/1 var(--mono, ui-monospace); |
| 2244 | cursor: pointer; | |
| 2194 | 2245 | } |
| 2195 | .date-submit { width: 100%; margin-top: 0.75rem; } | |
| 2246 | .drp-day:hover { background: color-mix(in srgb, var(--accent) 22%, transparent); } | |
| 2247 | .drp-day.is-out { color: var(--text-3); } | |
| 2248 | .drp-day.is-today { font-weight: 700; text-decoration: underline; text-underline-offset: 3px; } | |
| 2249 | .drp-day.is-range { background: var(--bar); } | |
| 2250 | .drp-day.is-edge { background: var(--accent); color: var(--accent-ink); border-radius: 8px; } | |
| 2251 | .drp-day.is-disabled { color: var(--text-3); opacity: 0.45; cursor: default; } | |
| 2252 | .drp-day.is-disabled:hover { background: transparent; } | |
| 2253 | .drp-day:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 8px; } | |
| 2196 | 2254 | .live-status { |
| 2197 | 2255 | display: inline-flex; |
| 2198 | 2256 | align-items: center; |
| @@ -2242,7 +2300,14 @@ summary:focus-visible { | ||
| 2242 | 2300 | .range-link { flex: 1 0 auto; } |
| 2243 | 2301 | .range-actions { justify-content: flex-start; } |
| 2244 | 2302 | .date-menu { position: static; } |
| 2245 | .date-popover { left: 0; right: auto; } | |
| 2303 | .date-popover { left: 0; right: auto; width: calc(100vw - 2rem); max-height: calc(100dvh - 6rem); overflow-y: auto; overscroll-behavior: contain; } | |
| 2304 | .drp { grid-template-columns: minmax(0, 1fr); } | |
| 2305 | .drp-presets { flex-direction: row; flex-wrap: wrap; gap: 0.35rem; padding: 0 0 0.6rem; border-right: 0; border-bottom: 1px solid var(--border); } | |
| 2306 | .drp-preset { width: auto; padding: 0.45rem 0.65rem; border: 1px solid var(--border); border-radius: 999px; } | |
| 2307 | .drp-cals { grid-template-columns: minmax(0, 1fr); } | |
| 2308 | .drp-cal.is-right { display: none; } | |
| 2309 | .drp-cal.is-left .drp-nav.is-hidden { visibility: visible; } | |
| 2310 | .drp-day { height: 2.5rem; } | |
| 2246 | 2311 | } |
| 2247 | 2312 | |
| 2248 | 2313 | /* Site switcher. Surface, border and radius come from .panel. The native menu remains |
| @@ -2434,29 +2499,7 @@ select.panel:hover { border-color: var(--accent); } | ||
| 2434 | 2499 | </nav> |
| 2435 | 2500 | |
| 2436 | 2501 | <div class="range-actions"> |
| 2437 | <details class="date-menu"> | |
| 2438 | <summary class="range-tool "> | |
| 2439 | <i class="i-hugeicons-calendar-03" aria-hidden="true"></i> | |
| 2440 | {{ isCustom ? (customFrom + ' to ' + customTo) : 'Custom' }} | |
| 2441 | </summary> | |
| 2442 | <form method="get" class="date-popover" aria-label="Custom date range"> | |
| 2443 | <input type="hidden" name="site" value=""> | |
| 2444 | @if (compare) | |
| 2445 | <input type="hidden" name="compare" value="1"> | |
| 2446 | @endif | |
| 2447 | <div class="date-grid"> | |
| 2448 | <div class="date-field"> | |
| 2449 | <label for="range-from">From</label> | |
| 2450 | <input id="range-from" type="date" name="from" value=""> | |
| 2451 | </div> | |
| 2452 | <div class="date-field"> | |
| 2453 | <label for="range-to">To</label> | |
| 2454 | <input id="range-to" type="date" name="to" value=""> | |
| 2455 | </div> | |
| 2456 | </div> | |
| 2457 | <button type="submit" class="date-submit is-active range-tool">Apply range</button> | |
| 2458 | </form> | |
| 2459 | </details> | |
| 2502 | <DateRangePicker :summary="rangeLabel" :active="isCustom" /> | |
| 2460 | 2503 | <StxLink |
| 2461 | 2504 | to="" |
| 2462 | 2505 | title="Overlay the previous period on the chart" |