A Day of UX Polish — From the Frame Right After Creating to a Card for Contributors
No new features. We filled the empty moment right after creating something, widened suggestions while emptying the screen, aligned how characters are counted, moved the time wheel off the screen edge, and told contributors the news for the first time. A day of nothing but polish.
A Day of UX Polish — From the Frame Right After Creating to a Card for Contributors
We built no new features today. Instead, twenty-four commits landed across the app and the server, every one of them working on an experience that already existed. The single frame where a list looked empty right after you created something. The fields that improvement suggestions could not see. An editor where line formatting vanished after an emoji. iOS pulling the whole screen down while you picked a time. And fecit saying nothing at all to people who were made contributors. Polish usually looks like adding, but today it was a mix of filling, widening, emptying, stepping back, and announcing.
1. The frame right after creating — a placeholder where the row will land
Create a task, close the editor, and for a moment the list has nothing in it until the server answers. It is brief, but it is long enough to make you ask “did that save?” Today we filled that moment. The instant the editor closes, a translucent, non-interactive placeholder row appears in the list, and when the real row arrives it takes over that spot.
Our first version pinned the placeholder to the top of the list, and the real row then jumped in somewhere else. So we changed the rule: the placeholder is drawn where the real row will land. Inside the in-progress band if there is one, otherwise wherever the sort order says. The helper that already slotted a just-completed task into the list got pulled out and shared with the placeholder.
We also caught a one-frame jump. If removing the placeholder and inserting the real row happen in different frames, the list flashes empty and refills. Moving the local-cache re-query from useEffect to useLayoutEffect put both in the same frame.
Then came scope. Rather than stopping at tasks, we laid the same grammar under twenty creation flows on mobile: memos, task and memo templates, projects, labels, contacts, the preparation catalog, milestones, steps, and the items inside a document’s detail — web links, preparation items, response rules, contact SNS accounts. Placeholder models also join the weekly, monthly, and daily calendar views and the kanban board. They use the same translucent grammar as routine ghosts, so nothing unfamiliar was added to the screen.
Two rules held throughout. Placeholder ids are render-only. They never enter the local cache or any request to the server. And in lists that reorder by drag, the placeholder is drawn outside the data. Kanban cards, step lists, response rules, preparation categories all compute neighbor order and send it to the server, and a placeholder inside the data array would leak into that computation. So those lists draw it in a footer instead. Guilds were the one exclusion: creation is paid and navigates immediately, so there is nowhere for a placeholder to stand.
2. Improvement suggestions — widen the signals, empty the screen
Fecit’s improvement suggestions compare a task’s execution record against its source template and pick out “what changed in this run that is worth folding back into the template.” Until now the comparison covered a handful of things: title, description, step contents.
Today we added eight more. The four intention fields (current state, hope, obstacle, stakes), location, reminders, difficulty, and step order. We initially left out current state and hope, reasoning that they vary per run and don’t belong in a template. The decision came back as “all of them.” Not narrowing a scope the user has set with our own classification was the right call. Step order went in carefully: it compares only when the steps form a single unbranched chain, and stays silent otherwise. No suggestion beats a wrong one.
One thing was confirmed as excluded: the start time slot. When a routine fires, the client sets the time to “the next full hour” and never reads the template’s start time. So folding a start time into the template would change nothing about the next firing. A suggestion that improves nothing when applied is decoration, so it stayed out.
The screen went the opposite direction. The “n/N selected” count row and the select-all and deselect-all buttons on the send screen were all removed. Everything is selected by default, so you just uncheck the items you want to leave out. The top bar and header are empty. And pressing “apply to template” from a writing-field editor now scopes the diff to that field alone; the full diff is still one tap away in the ⋯ menu. A full review of server, mobile, and desktop landed the same day, cleaning up small misalignments like a spinner that froze on a failed request, an “already sent” count that included other types, and a badge that drifted after a realtime reconnect. One policy was settled too: project members can send suggestions from their own records to the owner’s template, while applying immediately stays with the owner.
3. The unit for counting characters — formatting that vanished after an emoji
The mobile description editor is native, and iOS, Android, and JS each parse the same document format. The most painful bug from the review was this: type anything on a line after one containing an emoji, and the bullet or checkbox turns into a plain paragraph.
The cause was the unit. The iOS model counted offsets in visible characters, while JS, Android, and attributed strings all count in UTF-16 units. One emoji is 1 on one side and 2 on the other, so every position after it drifted and line types and format ranges came apart. We unified iOS offsets to UTF-16 everywhere. Android had a twin: a table line was counted as two characters, shifting every offset after a table by one. That was why checkboxes after a table wouldn’t toggle and a second table couldn’t be deleted.
Input got attention too. Pressing a toolbar command or tapping a checkbox mid-composition in Korean now commits the composition first, then runs the command. During composition the rebuild was a no-op, so the model changed while the screen didn’t. The value saved on blur is now the last document the native side handed over rather than the prop, fixing a case where autocorrect arriving in the same tick as blur saved a stale value. This module now has a few invariants for anyone touching it, and two matter most: offsets only through UTF-16 APIs, and commands during composition commit first.
4. Projects — what must not disappear, and what must
Projects got a full review as well: seven mobile fixes, six desktop, a few on the server. Here are the ones that read as UX.
On desktop, the workspace scope would occasionally snap back to “Me” on its own. The scope-validation code treated every error as “deleted or kicked,” so a brief network drop or a dev-server reload wiped the saved scope. It now falls back only on 403 and 404. The reverse also needed fixing: projects you were kicked from or that were deleted lingered in the list, and a realtime event now removes them immediately.
On mobile, tapping satisfaction in the review screen overwrote the result and retrospect you were typing with server values. Text in progress disappearing is not justified by any reason. The project row in the scope switcher gained a > button to open the project while keeping the current scope, and the assistant tab is hidden while in a project scope. Desktop gained a journal sub-view and an info modal to match mobile. One word was settled too: removing a member is a “kick.”
5. Drag the time wheel and the screen comes down — the culprit was outside the app
The report went like this: “In the date sheet, if I drag the time wheel the wrong way, that thing where the whole screen slides down turns on. Can we turn it off?”
The date sheet rises from the bottom, and the time wheel is its lowest element. Below the wheel there was only the safe-area padding for the home indicator. So when you grab the bottom item of the wheel and pull down, your finger starts very close to the bottom edge of the screen.
A downward swipe from that spot is iOS Reachability: the system feature that drops the whole screen halfway so the top is within one-handed reach. App code plays no part in it, and there is no API to disable it. Only the user can turn it off, in Settings.
So instead of turning it off, we moved away from it. Twenty points of padding under the wheel push the drag’s starting point out of the edge zone. Padding alone made the bottom of the sheet look empty, so we drew a half-point separator above it, the same one the sheet’s other rows use. Now that space reads as the sheet’s bottom band rather than a gap. The edge of the screen is not ours. There is a strip the system listens to first, and controls people touch often should stay clear of it.
6. Usage-time analysis — buildable is not the same as worth building
This one is not code. The question came up: what would a daily and weekly usage-time analysis look like?
First we split what “usage time” could mean. If it’s time with the app open, fecit has no goal of keeping people inside the app longer, so there is nothing worth showing. If it’s time from starting a task to completing it, the data already exists. The average completion time card on the statistics screen uses the same values, so drawing a seven-bar weekly card was not hard.
What we could not find was the point where that card makes results better. A task someone started and forgot, or one that spans several days, distorts a daily total badly. The average card already needed an explanation modal for that reason, and totals are more fragile than averages. And a number like “you did this many hours this week” tends to become a score for the person rather than feedback on one execution. The kind of number that hands out guilt in a thin week and changes nothing about the next attempt. So we passed. “We can build it” and “we should build it” are different sentences.
7. Becoming a contributor, and nobody telling you
The last was a genuinely empty spot.
Fecit has contributors: accounts an admin designates, which waive every free-tier limit. When we checked, designating someone did exactly two things on the server: flip a flag and write an admin action log. No realtime event, no notification, no on-screen moment. The person would only find out if they happened to visit settings, where a line reads “as a contributor, you can use everything without limits.”
The comparison was sitting right next to it. When an admin grants a pass, a card with bursting stars appears at the next launch. How that card decides whether you have “already seen it” became the backbone of this design. Passes track seen-ness per pass. A granted pass carries an “unseen” mark, and pressing a button on the card clears it. Purchased passes and older grants have no mark at all, so they count as seen, which is why no stale cards fire after a deploy. “Seen” means the moment the button was pressed, not the moment the card rendered.
A contributor is not something that stacks up as passes. It is a single state on one account, on or off. So we added one flag to the achiever document, and the card is decided by the combination of “is a contributor” and “has not yet seen the designation.”
| Situation | Card |
|---|---|
| Newly designated | Shows |
| Confirm button pressed | Hidden |
| Already a contributor, designated again | Hidden |
| Revoked | Hidden |
| Revoked, then designated again | Shows again |
The card itself speaks the same language as the pass card. Same star burst, same pop-in, one confirm button. We chose a heart for the icon, since contribution is a place for gratitude. Two lines of copy:
You’re now a contributor Thank you for building fecit with us. Every limit is now lifted for you.
A pass card and a contributor card can both be waiting in the same session, so the contributor card waits while a pass card is showing. Two cards bursting at once is noise rather than celebration. Mobile and desktop got the same card under the same rules.
The small things
Task lists and the kanban gained a label filter, and memos gained a creation-date range filter, on mobile and desktop. And a record arriving without a creation time used to crash the whole screen at the date divider; that is now guarded. The divider is skipped and the record attaches to the previous group, while a source tag records which path such a record came through. Defense and diagnosis come as a pair. Defend alone and the cause stays buried forever.
Today’s lessons
UX polish runs in many directions. Filling — the empty frame right after creating disappears when a placeholder sits where the real row will land. Widening while emptying — widen suggestions to exactly the scope the user set, and delete the buttons when the default is already right. Aligning units — when three parsers each implement one format, a single character-counting unit can quietly corrupt data. Stepping back — the screen’s edge belongs to the system and cannot be turned off. Not drawing — even with the data, if the number does not change the next attempt, keep it off the screen. Announcing — recognition has to correspond exactly to what actually happened, and before that, telling someone it happened is itself recognition. Starting today, contributors get the news.