Deadlines you can actually work on, and text you can actually read - #54
Merged
Merged
Conversation
The whole row was one button that marked the deadline done. People clicked it expecting the row to open, ticked something off instead, and watched it vanish — done deadlines are hidden, so the row disappeared under the cursor, which reads as the app losing it rather than as the app agreeing. The row body now opens a detail panel. Finishing something is a deliberate act: the tick box, which is its own small target, or the labelled "Mark as complete" button inside the panel. A deadline ticked off here also stays where it is, struck through, rather than being filtered out mid-click. Long enough to see what happened and to undo it. The panel holds what there was nowhere to put before: the full date and how long is left, why a date is estimated, a jump to that day in the calendar, and the student's own notes. Notes are new — they attach to any deadline by event id rather than to the underlying record, because most deadlines are not the student's to edit. A college's date is derived and a scholarship's belongs to the tracker; a note is the one thing that can hang on either, and it survives the event being re-derived on the next load. Saved when the field is left, not per keystroke, since every keystroke is a settings write. Week-strip events open their day too. They were not clickable at all, and at that size a tap is as likely to be a mis-tap, so they lead to the day panel where a deadline can actually be worked on rather than toggling anything. Every action inside an opened row names its deadline. Several rows can be open at once, and "Remove" alone says nothing about what it removes. The sidebar is called Overview.
Two things from the feedback on the first pass. The panel opened downwards, which pushed every row under it further down the rail and moved whatever the student was reading. It opens to the side now — to the left, since the rail sits on the right, falling back to the right and then to whatever fits. On a phone there is no room beside anything, so it becomes a sheet across the bottom. It is portalled to the body rather than rendered in place. The rail scrolls and its ancestors carry Framer Motion transforms; either would have clipped the panel or left it behind when the list moved. Position comes from the row's rect, measured when it opens and again on scroll and resize. Escape and a click outside close it. System deadlines now carry what they are actually about. A college brings its round and what that commits you to, where the application stands, how it sits on the list, how many of its tasks are done, where it is, and a link to the school's own page. A scholarship brings the award, what you marked it as, the type and who lists it. All of it is already on the records — the entry is a snapshot taken when the school was added, and the tracker row holds the rest — so an opened row costs no extra query. A scholarship also shows the catalogue's own wording where we had to work a date out of it. "May 1 (annual)" is the authoritative answer and our pinned date is a derivation; when the two differ the student should see both. The facts are a label/value list rather than typed fields per source. Colleges, scholarships and the student's own dates have different things worth knowing, and each derivation decides what earns the space instead of the row branching on category.
The tracker row holds a name, an amount and a deadline, and nothing about where to go. The catalogue row behind it has the URL and the funder, so the tracker select embeds both. One query, so opening a scholarship still costs nothing. This also fixes a line I got wrong in the last commit. I had labelled `source` "Listed by", and `source` is "curated" for every catalogue row — it records how the row reached us, not who is behind the money. That is now the provider: "National Merit Scholarship Corporation" rather than "curated". A student's own tracker entry has no catalogue row, so it gets neither, and the link and the funder simply do not appear.
Same move as the scholarship link, on the other side. deriveDeadlineEvents already looks the curated college up to find its real per-round dates, so everything below was sitting in scope unused. A school now shows what it admits, its sticker cost, whether it meets full need or runs a no-loan policy, and its CSS Profile date where it has one. A student opening an application deadline is deciding whether to spend tonight on this one or a different one, and the aid policies in particular change what a family pays by more than the sticker price suggests. The link no longer depends on the entry carrying a website. That snapshot is only taken for schools added through Discover; the ~50 curated ones had none, so the link was missing on exactly the schools we know most about. It falls back to the curated domain, and both go through one helper that copes with a bare domain, a trailing slash or an already-complete URL. Location falls back the same way, so a curated school stops showing a blank where its state should be.
Tapping an event in the week strip jumped to the calendar, which threw away the view the strip exists to give — the shape of the week. It now opens the same panel the Overview rail does, over the strip, with the week still behind it. The calendar is still one click away inside the panel, and "and N more" still goes straight there. Sharing that panel meant taking it out of DeadlineRow, which was the only thing that had one. The trigger shapes could hardly be less alike — a full-width row against a chip a few characters wide in a grid cell — and the only thing that differs is the anchor. So placement and dismissal moved to useAnchoredPanel, which the triggers own because they own the anchor, and DeadlineDetail is now just the contents. That also settles two things the linter was right about: the position was being measured from a ref during render, which would not have re-rendered had it changed, and ESTIMATE_HINT was a non-component export from a component file. It sits with the deadline data now, next to DEADLINE_TYPE_MEANING. Nothing tracks which panel is open. A click on another chip lands outside the first, which dismisses it, so one-at-a-time falls out of the dismissal logic rather than needing state in the strip.
The day labels in the week strip and the headings in Overview were hard to read, and measuring them showed why — the fault was in the tokens, not those two views. On the card background, --text-muted came out at 2.99:1 and --text-faint at 1.62:1, against the 4.5:1 WCAG asks for at these sizes. A past day in the strip was worse still: .wk-day--past dimmed the whole column to 0.55 opacity, which multiplies into the text as well and took its labels to 1.28:1. So the alphas move to where they clear the bar on every surface the app paints — muted to 4.5:1 for text people read, faint to 3:1 for chips and tags, which keeps the step between them. Dark needs its own values, light ink on a dark ground being a different problem, and it had been inheriting light's. The past-day dimming goes to 0.85, enough to read as past without dragging its text under. Three hardcoded colours failed too, and could not adapt because they were literals: #B93A3A dropped to 2.93:1 on dark, #C47A12 sat at 3.37:1 on light. They are now --c-late, --c-soon and --c-go, per theme. The year accents beside them are fills rather than text and are left alone. Day names, "clear", bucket headings and the date on a row move from faint to muted. They are content, not decoration. A test reads the tokens out of index.css and checks every one against every surface in both themes, so this fails when someone lightens a token rather than when someone forgets to update a copy of it. Confirmed it catches the old values rather than passing vacuously.
Clicking a tile only picked the day. Everything you might want to do with what was on it lived somewhere else, or nowhere. Entries in a day cell are now buttons that open the same panel the Overview rail and the week strip use: the details, the link to the school's or sponsor's own page, notes, mark complete, and remove for a date you set yourself. Reading one no longer changes which day is selected. That meant the cell could stop being one big button — it held buttons now, and a button cannot. Picking the day is its own control behind the entries, so clicking the space around them still selects the day, which is how a calendar grid behaves anyway. Adding your own date is possible from here at last, in two places: a + on a day when you hover it, and a button beside the day heading. Both prefill the day you are already looking at rather than making you type it again. The form is the one the Overview rail uses. It had been written into the panel; the calendar wanted the same three fields and only a different starting date, so it moved out to AddDeadlineForm. "Add to my calendar" is now "Download .ics", under a line saying it sends dates to Google, Apple or Outlook. The old label read as though it added something to this calendar. It also now says the file is a copy rather than a live link, which is the part most likely to be assumed otherwise — nothing syncs after the download.
A select, two checkboxes, a download button and two lines of explanation sat permanently open beside the page title, competing with it — for something a student does once a term, if that. My own doing: the explanation I added last commit was the right information in the wrong place, and it tipped a busy corner into a cluttered one. It is one "Export dates" button now. The options open in the same anchored panel a deadline uses, where there is room to lay them out down the panel rather than squeezed along the header, and room for the note about the file being a copy rather than a live link to sit next to the button it describes. The header goes back to a title, a subtitle and one control.
The list under the calendar runs the full width, so there is nothing for a flyout to sit beside — it opened over the month instead, hiding the thing the student had just clicked away from. That list opens a centred dialog now, over a scrim. Everywhere the trigger is narrow and has space next to it — the Overview rail, the week strip, an entry inside a day cell — keeps the flyout. No new dismissal logic: the scrim is outside the panel, so the click-outside handler that already closes a flyout closes this too. A modal claims aria-modal, which tells a screen reader the rest of the page is inert, so focus now moves into it and returns to whatever opened it. Saying that without moving focus would leave the keyboard in a page the reader has been told to ignore. The flyout is a disclosure, not a modal, and still leaves focus where it was.
"Remove" sat among the other actions at the same weight, took effect on the click, and never said what it was removing. It is the one thing on the panel that cannot be undone. It is "Delete" now, coloured as the destructive thing it is rather than only on hover, and it asks: "Delete 'Driving test'? This can't be undone", with the confirm and a way out. The button that actually deletes is the only filled red control on the panel. The other half of the confusion was a missing button rather than a present one. A date the student set on a college's task is theirs, and looks exactly like a date they added — but it belongs to that task, so removeOwn cannot find it and no Delete appeared, with nothing to explain the difference. The panel now says where that date lives and that clearing it there removes it here too.
Deploying timeline-prototype with
|
| Latest commit: |
4e5d9c0
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://ddec2f6e.timeline-prototype.pages.dev |
| Branch Preview URL: | https://ui-overview-sidebar.timeline-prototype.pages.dev |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ten commits from your feedback on the Overview rail, the week strip and the calendar. Each is scoped and explains its own reasoning, so they're worth reading individually.
The bug behind the first round of feedback
The whole row was one button that marked the deadline done. Clicking it ticked something off, and because done deadlines are hidden, the row then vanished under the cursor — which reads as the app losing it rather than agreeing with you.
The row body opens a detail panel now. Finishing something is deliberate: the tick box, or the labelled button inside the panel. Anything ticked off stays put, struck through, instead of being filtered out mid-click.
Where that panel goes
Beside the trigger, not under it — a rail is narrow, and pushing everything below down moves whatever was being read. It's portalled to the body, because rails scroll and their ancestors carry Framer Motion transforms, either of which would clip it or leave it behind.
The calendar's day list is the exception: it runs the full width, so there's nothing to sit beside, and it opens a centred modal instead. That one carries
aria-modal, so focus moves into it and returns to whatever opened it — saying the page is inert while leaving the keyboard in it would be a lie to screen readers.useAnchoredPanelowns placement and dismissal, held by the triggers since they own the anchor. Nothing tracks which panel is open: clicking another trigger lands outside the first, which dismisses it, so one-at-a-time falls out of logic that already existed.What's in the panel
Notes on any deadline — they attach by event id rather than to the underlying record, because most deadlines aren't yours to edit. A college's date is derived, a scholarship's belongs to the tracker; a note hangs on either and survives re-derivation. Saved on blur, not per keystroke.
System deadlines now say what they're about. A college brings its round and what that commits you to, where the application stands, task progress, location, and a link to the school's page. A scholarship brings the award, the type, who funds it, and where to apply. None of it costs a query — the entry is a snapshot from when the school was added, and the catalogue row is embedded in the tracker select.
A scholarship also shows the catalogue's own wording where we derived the date from it:
May 1 (annual)is authoritative and our pinned date is a guess at it.Contrast
Measured rather than eyeballed, after "the text colors for the days are not easy to see":
--text-mutedon card--text-fainton cardDark mode had never been considered separately — it redefined
--ink-rgbbut inherited light's alphas, and light ink on a dark ground is a different problem. Three hardcoded colours also failed and couldn't adapt:#B93A3Awas 2.93:1 on dark,#C47A123.37:1 on light. They're tokens now, per theme.contrast.test.tsreads the tokens out ofindex.cssand checks every one against every surface in both themes, so it fails when someone lightens a token rather than when someone forgets to update a copy. Verified it fails on the old values rather than passing vacuously.Calendar
Entries in a day cell open the same panel. That meant the cell stopped being one big button — it holds buttons now, and a button can't. Picking the day is its own control behind them, which is how a calendar grid behaves anyway.
Adding your own date works from here at last, prefilled with the day you're looking at, from a
+on hover or a button beside the day heading."Add to my calendar" read as though it added something to this calendar. It's "Download .ics" now, behind one Export dates button rather than a select, two checkboxes and two paragraphs permanently open beside the page title. It also says the file is a copy rather than a live link, which is the part most likely to be assumed otherwise.
Deleting
"Remove" fired on the click and never said what it would remove. It's "Delete" now, coloured as destructive, and asks first.
The other half of that confusion was a missing button: a date you set on a college's task is yours and looks identical to one you added, but it belongs to that task, so
removeOwncan't find it and no Delete appeared with nothing to explain why. The panel now says where that date lives.Also here
Discover can filter by region — the table's census regions are too coarse to browse by ("West" is Seattle, Phoenix and Honolulu), so this adds a finer set derived from state. The census map is untouched: it feeds the match score, and re-cutting it would silently restate everyone's results.
Testing
513 passing, lint and typecheck clean, build succeeds.
Two things worth knowing rather than discovering:
provider/urlembed is unverified against real data. PostgREST accepts the query shape, but with the anon key and no session RLS returns zero rows, so those fields have never populated end to end. The first real scholarship opened will confirm it.