Skip to content

feat(esign): signing UX decisions (2/2) - #128

Merged
lorenzocorallo merged 117 commits into
mainfrom
feat/esign-ux-decisions
Oct 22, 2026
Merged

lorenzocorallo merged 117 commits into
mainfrom
feat/esign-ux-decisions

Conversation

@lorenzocorallo

Copy link
Copy Markdown
Member

Second stacked PR, on top of #127. It builds the dashboard side of the decisions taken on the signing UX brief (17 choices).

  • New request:
    • "Sign a document" is gone; you add yourself with "Me", and when you sign alone or first, the primary signs right away.
    • Signers are always open, and delivery is a compact summary.
    • Uploading a PDF asks for the folder at once.
    • Signature spots come from the PDF's labels. Each box sits below its label, or beside it when labels are stacked, never over text; Presidente never matches Vicepresidente.
    • "Send again" on declined and cancelled requests opens the composer prefilled.
  • Request page: one action card on top. Activity has fewer events, the "Requested by … on …" line is gone, and signer numbers show only when the order counts. Decline asks for confirmation in red.
  • Signing hand-off:
    • After the PIN, the dialog stays open while the destination page loads under it. Then the eSign window and the dialog close, and the box fades into the stamp, with an outcome sentence and no toast.
    • It is the same from the request page, New request and a batch.
    • The shell's To sign count is right before the dialog closes.
  • Agent contract: the dialog waits for the agent's ready. With no answer it closes the window and explains, offering Start in a browser that has run the agent and Install elsewhere. It also covers "didn't start", "stopped" and "Update PoliNetwork eSign" (a direct download for the system). The proxy forwards the agent's User-Agent so the backend can refuse outdated agents.
  • Setup: direct downloads with stable file names. Pairing in CIE ID is the check at step 2. "Other systems and versions" is a quiet link.
  • Phones and tablets: review only. The sign buttons give way to a note, and an iPad is detected without a flash.
  • Desktop fallback removed: no more upload of a separately signed PDF.

pnpm check, pnpm typecheck, pnpm test and pnpm build pass. I tested it end to end against the backend and agent PRs: frame-by-frame hand-offs at 1366×768 and 1440×900, an emulated phone and iPad, and a real v0.1.1 agent build. Not checked yet: a real CIE card, the browser's real "Open PoliNetwork eSign?" prompt, Safari and Firefox.

Before deploying:

  1. Deploy the backend PRs first: fix(esign): signing UX pass (1/2) backend#91, then fix(auth): keep agent mode sign-in and sign-out off OIDC #92.
  2. Publish an agent release vX.Y.Z (not a pre-release) from feat(sign): signing UX decisions (2/2) esign#14. Until then, Setup's download buttons and the Update dialog link to files that return 404.
  3. No new @polinetwork/backend release is needed.

Still open: a real eSign help contact for Setup ("Card won't pair? Ask whoever sent you the request." for now).

Merge order: #127 first. It is squash-merged, so this branch then needs git merge -X ours origin/main, because the round-1 lines it changes again would otherwise conflict.

The unsigned agent is always blocked on a new Mac or Windows laptop, and the
setup page didn't say so. It now names the .dmg and .zip, the Open Anyway and
Run anyway steps, and how to check and start PoliNetwork eSign.
Clicking the dashboard behind the agent window used to land on the scrim, cancel the
signature and close the PIN window. Only the footer buttons and Escape end it now.
Nothing on a signer's computer is called "the agent", and the dashboard no longer
points to a missing button or repeats the PIN attempt count the eSign window shows.
A signer who has never signed on this computer learns about the one-time setup
before pressing Sign, not after it fails. The link opens a new tab so moved
boxes and batch review progress survive.
A role typed before the PDF or its labels loaded left the box at the default
spot, and changing the document reset the title, file name and folder. The
role now places its box whenever the labels arrive, and edits are kept.
A stopped request no longer draws pending boxes, says "Waiting" or "Will be
saved to". While delivery runs, Delivery shows Saving/Sending with a spinner and
switches to Saved as soon as Activity does.
…shed state (E17)

The dialog is 460px with a full-width footer and the shared motion, keeps its content
while it fades out, and once everything is signed says so instead of 'Type your PIN'
next to 'Stop here'. Escape no longer closes it while the page reloads.
Enter adds the highlighted person and moves focus to their role, whose
suggestions now use the dashboard's own list instead of the browser's
datalist. Reorder arrows appear only with two or more signers, and removing
a signer keeps focus in the list.
…en (E15)

Closing the eSign window now shows 'The PoliNetwork eSign window was closed' within a
poll instead of 'Type your PIN' for ten minutes. 'Show the window' focuses the open one
instead of opening a second PIN form, so Cancel leaves no window behind.
In dark mode the window no longer flashes a white 'Opening PoliNetwork eSign…' page
before the agent's dark form: the placeholder uses the agent page's own colours.
…n Back

The requests list names the person an in-progress request waits on, To sign
drops the Status column, ?status= survives opening a row, and the request page
shows when each waiting signer was emailed or why they couldn't be.
When someone is signing the request right now, the Cancel request dialog says
they won't be able to finish.
A co-signer blocked by someone signing the same request no longer has to reload:
the request and batch pages check every 3 s while the tab is visible, keep the
moved boxes, and enable signing as soon as the other signature ends.
A blocked submit used to show one problem at a time in a banner that could
render below the fold. Each problem now appears at its field, and focus
moves to the first one; a refused create scrolls its error into view.
Signatures › To sign on the Overview now carries the same count pill as the
panel, so someone landing there sees waiting signatures without opening
Signatures. Reports › Open uses the same shell count.
Cancelling in the eSign window now just closes the dialog. A request cancelled or
declined by someone else, a link that expired (with Start again) and an error that
ends the job (PIN blocked, CIE not set up, with the setup guide) each say what
happened, and closing reloads the page so it no longer offers a stale Sign button.
… (E4)

When the eSign window reports that the agent behind it died, the dialog swaps 'Type
your PIN' for how to start it and open the window again. The message is documented in
the contracts, as the first postMessage detail the dashboard acts on.
… access impact (C6, C9)

The backend adds these fields and the accessImpact query; the contracts list them first
so the dashboard work can follow once the package is released.
Leaving the new-request page through a link, the rail, the command palette,
browser Back or a reload threw away the document, signers and boxes. It now
asks first; moving to the request just created is never interrupted.
The output row cut the folder name off after the drive label. It now shows
the folder over where it is, and both drive browsers open in the folder
used last instead of the drive's top folder.
Add place pushed the PDF down with a banner just as the pointer aimed at
the page. The hint now replaces the page counter in the viewer's toolbar,
with a Cancel button, and Esc leaves placing mode.
Step 4 said the status page "should say it's ready to sign", but the only released agent (v0.3.0) shows "The agent
is running" and never says ready; only the next agent release's status page says "Ready to sign". Since the admin may
ship before that release, the step now says the page should show that PoliNetwork eSign is running and ready to sign,
which fits both. TRACKER.md records the dependency so the step can quote "Ready to sign" once the agent is released.
A box put on the wrong page (an annex, say) left the signer with signing
in the wrong place or declining. "Move to another spot" now waits for a
click on any page, as in the composer, on the request and batch pages.
…e (E30)

Close on an ended attempt waits for router.invalidate({ sync: true }) before
the dialog closes, so the page shows the cancelled or failed request as it is
now. During that wait the button gave no feedback and Start again ignored
clicks silently. Close is now a LoadingButton (spinner, disabled, aria-busy)
for the whole reload and the fade-out, and Start again is disabled meanwhile,
as docs/design.md asks of pending actions.
Next document used to fetch the PDF after the click, then smooth-scroll the
box over blank paper. The batch now fetches the next document in advance,
the viewer jumps to the box, and zooming keeps the middle of the view.
Moving a signer with Move up/Move down from the keyboard dropped focus to
the page body: the moved row's DOM node is reinserted, and an arrow that
reaches the end of the list becomes disabled. A layout effect now puts the
focus back on the same arrow of the moved row, or on the opposite arrow
when the row reached the top or bottom. It only acts when the arrow had
the focus, so a mouse click in Safari moves no focus.
…her"

A single signer's row in To sign and Requests repeated "0 of 1 signed" beside
its one avatar, which the request page had already dropped. To sign's header
counted every waiting document ("Sign 69 together") while the batch it opens
holds ten at most and says how many more wait: it now names what it signs, as
the request page's card does.
Send again showed the empty composer's skeleton, a file list and a dashed
dropzone, for about half a second before the prefilled document, as if the PDF
had to be chosen again. With `?from=` the pending state is a page and the
request's column, as the composer then shows.
An iPad asks for the desktop site with a Mac's User-Agent, so the server
rendered the full macOS guide, which collapsed to the phone state after
hydration. Like the request page, Setup now renders both variants for a Mac's
User-Agent until hydration, and CSS shows the one for the screen.

On a phone the message to open the page on the computer leads as an info
alert, instead of a grey subtitle and a line under step 1, and the download is
an outline button: it is no use on the phone. Step 4 drops "Page doesn't
open? Start PoliNetwork eSign…", which the Start button beside it already says.
…decline

Declining a signature request stops it for everyone, like cancelling it, but its
confirmation had no danger tile and a blue submit, while Cancel request has
both. FormDialog takes `tone="danger"` (with an `icon`): the confirmation's
danger tile before the title and a solid danger submit. "Decline request?" uses
it with the X of the card's Decline button.
After a keyboard action (Decline sent with ⌘↵, a signature started with Enter)
focus moves to the card's sentence for screen readers, and the global
:focus-visible ring drew a square box around the text, as if it were a field.
The box left over a place just signed kept its see-through fill, so once the
new version was drawn the stamp's text showed through the blue box for about
half a second before the fade. On the white sheet the same colour is now
opaque: only the fade reveals the stamp.
…ge says it

Sending a request without signing landed on its page, whose card says whom it
waits for and that they were emailed, under a "Request sent." toast saying it
again. No success toast where the page states the outcome; a signature that
couldn't start still gets its partial-failure toast.
The request and batch pages numbered the boxes and the Signers rows of every
request with several signers, then added "Signers can sign in any order." to
undo the order the numbers implied; New request numbered only ordered ones, so
the same request looked different there. Numbers now show only when the request
is ordered with two or more signers; otherwise a row shows the signer's
initials, and the "any order" line goes.

In New request the rows keep the number column from the second row on, while
ordered, and the digits fade in as people are added, so picking the second
person no longer moves every role and name sideways.
…loads

Signing from New request closed the agent window and the dialog on the
composer, then opened the new request's page, which drew its PDF only after it
mounted: for about half a second the viewer showed blank sheets, with no box
dissolving into the stamp, and focus was left on the body. A batch's dialog read
"Document signed" as it closed, then the navigation to To sign cut its exit.

New request and the batch now use a dialog held in the dashboard layout
(`EsignSigningHost`, `useHostedSignWithCie`), so their hand-off can navigate
while the dialog and the agent window stay up:

- New request fetches the new request and its signed PDF behind the dialog,
  then opens its page, which starts drawing at once, keeps the signed boxes over
  their spots, and says when the signed spot is drawn (`landing.ts`). Only then
  do the agent window and the dialog close; the boxes dissolve into the stamps,
  and focus goes to the card's sentence, as when signing from the request page.
- The batch opens To sign under the dialog, which finishes its exit there, and
  toasts as it closes. The signed state keeps its document count, so the title
  stays "3 documents signed" while the dialog closes without its jobs.

`useSignWithCie` reads its options when it needs them (`useSigning`), so the
host can follow the page that started even after that page is gone, and learns
per attempt whether its documents were created in the click.
…ndow open

When the agent served its page but the page's own job call couldn't reach it,
the page posts `agent_unreachable` as a fatal error. The dialog handled every
fatal error first, so it said "PoliNetwork eSign can't continue" with the
agent's words, which name a Sign button that isn't there. `agent_unreachable`
now means the agent stopped, fatal or not: "Start PoliNetwork eSign".

A job that failed with `agent_outdated` closed the agent window unless its fatal
message had already arrived. The backend fails the job before the page learns
why, so a poll in between closed the window of an agent that had said `ready`
and was about to show its update screen. The window now closes only when the
agent neither said `ready` nor posted a fatal error.
"Unblock it with your PUK in CIE ID or the CieID app" named two apps a letter
apart without saying which is which; it now says, as the agent does, "in the
CieID app on your phone or in CIE ID on this computer".

The Update state told every system to download the new version and open it,
while on Linux PoliNetwork eSign may come from a package manager and the agent
window beside it says to update the polinetwork-esign package. The body now
depends on the system, in the agent's words ("it replaces this one").
…action

- "It's already installed: start it" sat inside the dialog's description, in its
  muted small type with no icon, and read as a caption: someone who installed
  PoliNetwork eSign in another browser would reinstall it. It is now a ghost
  button under the words, in the text colour with the Play icon, outside the
  description and its live region; the Setup guide link of failed jobs too.
- After a Start that got no answer the dialog showed the Install state again
  with nothing new. It now says "PoliNetwork eSign didn't start", "If it's
  installed, open it yourself, then try again.", with Try again and a quiet
  Install link.
- Install turning into Try again keeps the button's width, so Cancel signature
  doesn't slide under the pointer.
- Start keeps its blank "Starting PoliNetwork eSign…" window 2 s before loading
  the agent's page (was 0.8 s), so a cold launch shows the browser's "can't be
  reached" page less often.
- A browser that has never run PoliNetwork eSign waits 4 s for it instead of
  2.5 s: its agent may be an old one, which shows only once the backend has
  served its job, and every browser lacks the flag on the first signature after
  the deploy. A browser known for a new agent keeps 2.5 s, one known for an old
  agent 6 s.
…e current agent routes

contracts.md still listed `GET /api/job`, missed `/api/cancel`, `/api/forget`,
`remember` and the status's `card`, and said agent_outdated came only from
prepare. It now says which User-Agent the proxy forwards, that loading a job
answers 426 `agent_outdated` below MIN_AGENT_VERSION (0.2.0) and ends the job
failed and seen, what passes the check, the fatal `agent_unreachable`, when an
outdated agent's window closes, and the 4 s wait for a browser new to the agent,
with its trade-off for old agents on a slow link.
… UI rules

TRACKER logs what the integrated admin, backend and agent branches changed, the
"Sign N together" wording kept on purpose, and the release order: the backend
before the dashboard (the decline dialog promises the creator's email), and the
agent release, or renamed copies on v0.3.0, before Setup's downloads.

design.md describes the destructive FormDialog, the order numbers only where the
order holds, the quiet action under the signing dialog's words, the hand-offs
held above the routes, the ringless card sentence, Setup's phone and iPad
renders, and boxes beside stacked labels.
…atch hand off

Both hand-offs navigated without re-running the /dashboard loader, so the To
sign badge kept counting the documents just signed until a full reload (after a
batch, 104 in the shell while To sign said 102). They now reload every loader
once the next page is open, as the request page's own hand-off does, so the
badge is right before the dialog closes. New request reloads while the request
page draws the signed spot; a failed reload is logged and doesn't hold the
dialog.
The field kept 13px more than "Me" takes, and with the order column the
placeholder clipped to "Choose signe" at every desktop width. "Me" is a little
narrower and the field leaves it just its room, and below 1040px the role column
is 108px, which still fits "Vicepresidente".
At 460px, a confirmation's width, the signing dialog wrapped its longest title
onto two lines. It is now 520px wide, so every state's title holds one line;
other dialogs keep their widths.
@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: fd2a1b2b-4958-4959-8f6c-11af151a8efa
📥 Commits

Reviewing files that changed from the base of the PR and between 451f338 and 7745a31.

📒 Files selected for processing: 62
  • docs/design.md
  • docs/esign/TRACKER.md
  • docs/esign/contracts.md
  • src/components/primitives/combobox.tsx
  • src/components/primitives/confirm-dialog.tsx
  • src/components/primitives/form-dialog.tsx
  • src/components/primitives/index.ts
  • src/components/primitives/inline-edit.tsx
  • src/components/primitives/inline-link.ts
  • src/components/primitives/kbd.tsx
  • src/components/primitives/skeletons.tsx
  • src/components/shell/index.ts
  • src/components/shell/page-bar.tsx
  • src/components/shell/panel.tsx
  • src/features/dashboard/overview-page.tsx
  • src/features/esign/action-card.tsx
  • src/features/esign/agent.ts
  • src/features/esign/batch-page.tsx
  • src/features/esign/composer-fields.tsx
  • src/features/esign/composer.ts
  • src/features/esign/delivery-rows.tsx
  • src/features/esign/document-picker.tsx
  • src/features/esign/drive-browser.tsx
  • src/features/esign/esign.functions.ts
  • src/features/esign/landing.ts
  • src/features/esign/new-request-page.tsx
  • src/features/esign/pdf-viewer.tsx
  • src/features/esign/places.ts
  • src/features/esign/placing.tsx
  • src/features/esign/request-card.ts
  • src/features/esign/request-page.tsx
  • src/features/esign/request-status.tsx
  • src/features/esign/requests-page.tsx
  • src/features/esign/requests.functions.ts
  • src/features/esign/setup-page.tsx
  • src/features/esign/sign-with-cie.tsx
  • src/features/esign/signature-box.tsx
  • src/features/esign/signer-rows.tsx
  • src/features/esign/signing-device.ts
  • src/features/esign/signing-host.tsx
  • src/features/esign/signing-state.ts
  • src/features/esign/use-fill-height.ts
  • src/features/esign/use-reload-while.ts
  • src/lib/api/types.ts
  • src/lib/format.ts
  • src/router.tsx
  • src/routes/api/esign/agent/$.ts
  • src/routes/dashboard.tsx
  • src/routes/dashboard/esign/inbox.tsx
  • src/routes/dashboard/esign/new.tsx
  • src/routes/dashboard/esign/requests/$requestId.tsx
  • src/routes/dashboard/esign/requests/index.tsx
  • src/routes/dashboard/esign/sign.tsx
  • src/routes/dashboard/index.tsx
  • src/styles.css
  • tests/esign-composer.test.mjs
  • tests/esign-device-detection.test.mjs
  • tests/esign-mutation-refresh.test.mjs
  • tests/esign-places.test.mjs
  • tests/esign-request-card.test.mjs
  • tests/esign-signing-state.test.mjs
  • tests/server-security.test.mjs
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

… saved

localStorage.setItem can throw (storage full or blocked). The write sat in importFromDrive's try block after adopt had staged the document, so a failure showed "Couldn't open the PDF." and skipped setLastFolder. Remembering the folder is now best-effort.
The quoted text is the start of the macOS notice, not a label next to the button. Match the agent README: Open Anyway sits next to the message about PoliNetwork eSign.
…ign-ux-decisions

# Conflicts:
#	src/features/esign/setup-page.tsx
Matches the agent's update screen, which now names the step after the download: open the app inside the DMG, or extract the zip and open esign.exe.
Base automatically changed from feat/esign-ux-pass to main October 22, 2026 10:10
@lorenzocorallo
lorenzocorallo merged commit 160936d into main Oct 22, 2026
1 check passed
@lorenzocorallo
lorenzocorallo deleted the feat/esign-ux-decisions branch October 22, 2026 10:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant