Skip to content

PT-4363: Internet & Connectivity settings UI refinements - #2695

Open
katherinejensen00 wants to merge 4 commits into
mainfrom
pt-4363-internet-settings-ui-refinements
Open

PT-4363: Internet & Connectivity settings UI refinements#2695
katherinejensen00 wants to merge 4 commits into
mainfrom
pt-4363-internet-settings-ui-refinements

Conversation

@katherinejensen00

@katherinejensen00 katherinejensen00 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

PT-4363: Internet & Connectivity settings UI refinements

Base: origin/main · Files: 28 source + 17 generated dist/ · Reviewed with: Claude Opus 5 · Date: 2026-08-19

Summary

Refinements to the Internet & Connectivity settings, which appear in two places — the standalone web
view and the first-run wizard step. Both are driven by shared platform-bible-react components, so
most changes land once and apply to both.

  1. Option descriptions → hover/focus tooltip, signalled by a new info icon on each row. The
    always-visible description paragraphs are gone.
  2. The wizard step gains the "Internet & connectivity" headline the standalone view already had,
    plus a short subtitle.
  3. The developer server picker becomes radio buttons instead of a toggle group, and Test gets
    its own row
    alongside Production and Development.
  4. The "Disabled options are planned for future updates" footer is removed — the per-row "Coming
    soon" badge already says it, and dropping the line keeps the wizard's Next button in view.

Two things surfaced while building it, and are included:

  • A latent bug the wizard would have hit. The first-run gate is an opaque dialog at
    Z_INDEX_FIRST_RUN (700); tooltips portal to document.body at Z_INDEX_TOOLTIP (550). Any
    tooltip in the wizard would have painted behind the gate — silently defeating change 1 in half
    the places the ticket asks for it. Fixed by portalling tooltips into the gate.
  • That workaround already existed for popovers, so both now build from one shared factory instead
    of two copies inside vendored shadcn files.

Also folded in: a shared WizardStepHeading, with all five wizard steps converged onto it —
the wizard previously rendered step headings at two different font weights.

Reviewing this efficiently

  • Read these two first — they carry the substance:
    advanced/internet-access-option-list/internet-access-option-list.component.tsx and
    components/portal-container.context.tsx.
  • first-run-overlay.component.tsx looks huge and isn't. ~150 changed lines, ~14 real — the
    rest is indentation from wrapping the dialog's children in a provider. Use git diff -w, or the
    "Hide whitespace" toggle in GitHub's diff view.
  • Skip lib/platform-bible-react/dist/ (17 files) — generated by npm run build:pbr, committed
    because the package tracks its build output.

What changed, by file

Shared components — lib/platform-bible-react

File What it does
advanced/internet-access-option-list/…component.tsx Descriptions → row tooltip; adds the info icon and an sr-only copy wired to each radio via aria-describedby; drops the footer; instance-scoped ids
advanced/developer-section/…component.tsx ToggleGroupRadioGroup; Test gains its own row; isSurfaced() decides which values display as themselves vs collapse to Production
shadcn-ui/tooltip.tsx New export TooltipPortalContainerProvider — the z-index fix
shadcn-ui/popover.tsx Collapsed its duplicate portal workaround onto the shared factory (behavior unchanged)
components/portal-container.context.tsx New, internal. createPortalContainerContext() — one definition, one context per primitive
src/index.ts One added export

Wizard — src/renderer/components/first-run

File What it does
first-run-overlay.component.tsx Wires the tooltip provider at the gate, so every step gets working tooltips (~14 real lines)
steps/internet-settings-step.component.tsx Renders headline + subtitle across all states, so they don't pop in when the provider resolves
wizard-step-heading.component.tsx New. One definition of the step <h2>, used by WizardStepForm and directly by steps that own their layout
steps/language.component.tsx Converged onto WizardStepHeading
steps/sync-progress.component.tsx Converged both branch headings (in-progress and complete) onto WizardStepHeading, retiring the last hand-rolled font-medium step heading

Localization — extensions/src/paratext-registration/contributions/localizedStrings.json

Change Key
Added (en + es) %internetSettings_subtitle_short% — the wizard step's subtitle
Deprecated + removed (en + es) %paratextRegistration_internetUse_footer%, with a deprecationInfo note and no replacement key

The removal follows the immutability convention: a deprecationInfo entry records why, rather than
editing a live string's meaning in place.

Tests & stories

New: tooltip.test.tsx, popover.test.tsx (both portal targets, both verified falsifiable by
sabotage), tooltip.test-utils.ts, wizard-step-heading.component.stories.tsx, and a
bundled-extension Storybook story for DeveloperSection that renders it with real localized strings.
Updated: both component tests, the first-run overlay test (now guards the z-index wiring), the
language and internet-settings step tests, and both lib story files.

E2E — e2e-tests/tests/isolated

internet-settings.page.ts grew a page object; internet-settings.spec.ts was refactored onto it.
This spec was previously broken — see Needs a decision #3.

Needs a decision from you

  1. Tooltip guideline deviation — for Alex. Guidelines/Tooltips says "a tooltip is a hint, not
    an explanation"
    and its Avoid example is 68 characters; these descriptions run 67–162
    characters
    , up to two sentences. The author chose to keep the full descriptive copy (shortening
    needs new strings) and add the info icon for discoverability. This wants explicit UX sign-off,
    not silent acceptance.
  2. Guidelines/Product names is wrong. It states "'Send/Receive' is now called Sync". Per the
    author these are distinct operations — Sync follows up a Send/Receive by also handling
    connected resources and model texts. As written, that page will keep prompting engineers to
    rename Send/Receive → Sync across the 10 strings that legitimately use it. Route to its owner.
  3. The e2e spec has likely never run green. internet-settings.spec.ts sits at the top level of
    tests/isolated/, which run-isolated.mjs cannot select (it enumerates subdirectories), and CI
    runs only test:e2e:smoke. That is how a call to FrameLocator.filter()a method that does
    not exist
    — survived in it. This PR fixes that call and a case-sensitivity bug beside it, but
    wiring the suite into a real run is a team decision.

Verification

Check Result
npm run typecheck ✅ clean
npm run lint (repo-wide, incl. lib workspace) ✅ clean
npx typedoc ✅ exit 0
src/renderer/components/first-run ✅ 128 pass
platform-bible-react unit (jsdom) ✅ 399 pass / 40 files
platform-bible-react storybook (chromium) ⚠️ no trustworthy reading — see below
Wizard tooltip rendering above the gate ⚠️ never seen running — see below

Two gaps, stated plainly:

  • The wizard tooltip fix has not been visually confirmed. The app will not start in this worktree
    (dev-packages/scripture-editors has ~900 dirty entries, so link-dev-packages aborts and takes
    npm start with it). The fix is reasoned, unit-tested at the mechanism level, and
    regression-guarded at the wiring level — but nobody has seen it. Please eyeball it on a working
    machine.
  • The Storybook browser project could not be measured here. Two consecutive runs on identical
    source
    gave 8 failures / 1047 tests collected, then 26 / 685 — machine saturation, not code. The
    giveaway: badge.stories.tsx > Default is a static render with no play function and it failed on
    Test timed out in 15000ms, i.e. the runner never reached it. The deterministic unit project
    covers every component this branch touches and is green. Let CI arbitrate the browser project.
Full review record — findings, API surface, interview notes

API Changes

  • Added TooltipPortalContainerProvider (shadcn-ui/tooltip.tsx, re-exported from
    src/index.ts) — props { container: HTMLElement | null; children: React.ReactNode }.
  • PopoverPortalContainerProvider — implementation moved from a function declaration to a const
    bound to the shared factory; emitted declaration changes from export declare function … to
    export declare const …. Props type and runtime behavior unchanged.
  • New internal module src/components/portal-container.context.tsx exporting
    createPortalContainerContext — deliberately not exported from src/index.ts or the
    experimental entry point.
  • DEVELOPER_SECTION_STRING_KEYS — gained %paratextRegistration_label_serverType_option_Test%.
    Consumers pass the resolved map straight through, so no consumer change is required.
  • INTERNET_ACCESS_OPTION_LIST_STRING_KEYS — dropped the footer key. Still derived from
    OPTION_ROWS, so it stays in sync automatically.
  • DeveloperSectionProps / InternetAccessOptionListProps — no type changes; TSDoc wording only.
  • WizardStepHeading — new, renderer-internal, not a published package API.
  • lib/platform-bible-utils, lib/papi-dts/papi.d.ts, extension *.d.ts: no changes.

No exports removed and no signatures narrowed.

Findings

23 findings across four analysis passes (API/correctness, style/patterns, coverage/compliance, UX).

Critical — all fixed in review

  • TypeDoc build break. {@link createPortalContainerContext} could not resolve (the factory
    is internal) and typedoc.json sets invalidLink: true + treatWarningsAsErrors: true, so
    npx typedoc exited 4 — breaking npm run build for the package and the publish-docs CI
    job on merge. (fixed: inlined a self-contained contract on both providers, which also resolved
    the TSDoc finding below; typedoc now exits 0)
  • @typescript-eslint/no-shadow ×2radioId/descriptionId parameters named value
    shadowed the value prop. (fixed: renamed to optionValue)
  • no-type-assertion on row as HTMLElement in the hover story. (fixed:
    querySelector<HTMLElement> + explicit throw, which also made that story falsifiable)

All three were introduced by the author's own final edits and missed by earlier checking — see
Verification gaps below.

Important

  • Tooltip copy violates Guidelines/Tooltips. (Author kept the descriptive copy
    deliberately. Escalated to Alex — see Needs a decision #1.)
  • Nothing signalled the descriptions existed — no icon, just a 300 ms dwell, so anyone
    clicking through never saw one. (fixed: Info icon per row. Deliberately decorative —
    aria-hidden, no tab stop — since making it the sole trigger needs an accessible name, i.e. a
    new localized string, plus five extra tab stops. The whole row stays the trigger.)
  • Public-API TSDoc stopped standing on its own, pointing at an unexported symbol;
    PopoverPortalContainerProvider lost its inline contract.
    Code-Style-Guide.md#api-surface-tsdoc-requirements requires these to be self-contained.
    (fixed: contract inlined into both)
  • WizardStepHeading had no story — the only component in first-run/ without one.
    (fixed: three stories, incl. WeightMismatchToday showing the remaining weight mismatch)
  • The z-index fix had no regression guard. The overlay test stubbed the provider as a
    pass-through and ignored the new ref, so deleting either left all tests green. (fixed: the
    stub records its container, DialogContentStub forwards its ref, and a new test asserts both.
    Verified falsifiable — removing the ref fails it; removing the wrapper fails it.)
  • Bare provider aliases share a runtime name in React DevTools and use an anonymous props
    type.
    (Cosmetic/dev-tooling only; TSDoc now carries the contract. Left as-is.)

Minor

Fixed: developer-section row padding now matches the option rows · row-wide hover highlight now
matches the clickable area in both lists · raw <label> vs shadcn Label asymmetry documented
(Label's peer-disabled: is a sibling combinator and the option-list label is nested two
levels down, so Label genuinely cannot work there) · unused export default removed · React
act() warning fixed · added popover.test.tsx (verified falsifiable) · the
[data-slot="tooltip-content"] selector de-duplicated across the two vitest harnesses.

Dismissed: "Send/Receive" → "Sync" — the guideline is wrong, not the strings (see Needs a
decision
#2).

Open, left as reviewer judgment: coming-soon rows are disabled and so unreachable by sighted
keyboard-only users (screen readers get the sr-only copy; mouse users get hover) · placement of
portal-container.context.tsx in components/ vs the existing src/context/.

Template propagation

None. No changed file carries a #region shared with marker.

Positive observations

  • The ToggleGroupRadioGroup swap matches Component-Selection-Quick-Reference.md.
  • The subtle correctness risk in that swap — a radio fires no change event when the already-checked
    item is clicked, stranding a user on a hidden server value with no one-click route back to
    Production — was caught, handled, documented at the call site, and covered by tests including a
    negative case asserting a Test user is not silently re-routed.
  • data-testids survived the swap, and the e2e spec adds toHaveRole('radio') so they can't drift
    back onto a different control.
  • Hardcoded DOM ids → useId(), fixing real duplicate-id collisions on Storybook autodocs pages.
  • Every changed line in the vendored tooltip.tsx/popover.tsx sits under a // CUSTOM: comment.
  • The removed footer string was deprecated with a deprecationInfo note rather than deleted
    silently, matching the string-immutability convention.
  • Tests explain why an assertion is structural rather than visibility-based (jsdom loads no
    stylesheet; Playwright counts sr-only as visible) — the kind of note that stops a correct
    assertion being "simplified" into a wrong one later.
  • The e2e spec asserts toBeVisible() on [data-slot="tooltip-content"] rather than
    getByRole('tooltip'), which Radix puts on a visually-hidden copy — precisely the assertion that
    catches the bug this PR fixes.

Interview notes

Author decisions during review: keep the full descriptive tooltip copy and add an info icon
(knowing deviation from the guideline, not an oversight) · leave proper-names.mdx alone despite its
error · do not rename the Send/Receive strings · track the remaining heading convergence as
PT-4402 · land everything as one PR.

The Send/Receive correction is worth highlighting. The analysis assumed Sync was simply the new
name for Send/Receive, following the written guideline. The author corrected it: they are distinct
operations. That invalidated the finding and redirected it at the guideline page instead. No areas
were deferred to AI or left unexplained; no unresolved items.

Verification gaps this review exposed

These explain how three build-breaking issues survived earlier "all green" checks:

  1. eslint --cache masked real errors. A repo-root eslint . reported clean while the
    lib/platform-bible-react workspace — which npm run lint also runs, with stricter rules — had 3
    errors. Clearing .eslintcache and running the workspace lint directly surfaced them.
  2. build:basic skips TypeDoc. npm run build is build:basic && lint-fix && typedoc; only the
    full script validates doc links, so the TypeDoc break was invisible to every build:basic run.

Follow-up

  • PT-4402 — completed in this PR. Originally deferred, then finished once it was clear the two
    SyncProgressStep headings are mutually exclusive branches of a single step title ("Syncing your
    projects." → "Sync complete"), already marked up as <h2>; the role="status" wrapper governs
    screen-reader announcement, not heading semantics. Leaving one step at font-medium while the
    other four moved to font-semibold would have shipped a lone outlier — worse than either
    consistent state. All five steps now share WizardStepHeading, and the deferral note plus the
    WeightMismatchToday story that documented the mismatch are removed.

AI-assisted — session


This change is Reviewable

katherinejensen00 and others added 3 commits August 22, 2026 17:26
Option descriptions move from always-visible body copy into a hover/focus
tooltip, signalled by a new info icon on each row; the wizard step gains the
"Internet & connectivity" headline and a short subtitle; the developer server
picker becomes radio buttons with Test on its own row; and the "Disabled
options are planned for future updates" footer is removed, since the per-row
"Coming soon" badge already says it.

Two things surfaced while building it and are included:

- The first-run gate is an opaque dialog at Z_INDEX_FIRST_RUN (700) while
  tooltips portal to document.body at Z_INDEX_TOOLTIP (550), so any tooltip in
  the wizard would have painted behind the gate. TooltipPortalContainerProvider
  portals them into the gate instead.
- That workaround already existed for popovers, so both primitives now build
  from one shared createPortalContainerContext factory rather than two copies
  inside vendored shadcn files.

Also adds a shared WizardStepHeading and converges four of the five wizard
steps onto it; PT-4402 tracks the remaining SyncProgressStep headings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Session-URL: <session URL>
Its two headings — the in-progress and complete branches — were the last
hand-rolled step headings in the wizard, at tw:font-medium while the other
four steps had moved to WizardStepHeading's tw:font-semibold. Shipping one
outlier out of five is worse than either consistent state.

They are step headings despite sitting inside role="status": the two are
mutually exclusive branches of a single step title ("Syncing your projects."
-> "Sync complete"), they were already marked up as <h2>, and role="status"
governs screen-reader announcement rather than heading semantics.

Retires the deferral note in WizardStepHeading's TSDoc and the
WeightMismatchToday story, which existed only to make the mismatch visible.

Not visually verified — the app does not start in this worktree. See PT-4402
for the revert path if UX considers these a distinct class of heading.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Session-URL: <session URL>
dist/ conflicted again on this rebase — main rebuilt the bundles too, and the
hash-named resizable chunks were renamed on both sides. Resolved by taking
main's dist wholesale and regenerating it from the rebased source, rather than
hand-merging minified output.

Verified the rebuilt output carries both sides (TooltipPortalContainerProvider
from this branch, the Empty exports from main) and that every entry point —
index.js/.cjs and experimental.js/.cjs — references only chunks present on disk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Session-URL: <session URL>
@katherinejensen00
katherinejensen00 force-pushed the pt-4363-internet-settings-ui-refinements branch from 85a1038 to cc36191 Compare August 23, 2026 01:35
…n gate

Nothing demonstrated the case the portal fix exists for: a tooltip opened
inside the opaque gate, which stacks at Z_INDEX_FIRST_RUN (700) above the
Z_INDEX_TOOLTIP (550) layer that tooltips portal into. Without
TooltipPortalContainerProvider such a tooltip paints behind the gate and is
invisible, and no story made that visible.

Uses FirstRunGate's documented stepComponents override to inject a body that
renders the real InternetAccessOptionList. The real step cannot be used here:
it waits on a PAPI data provider that has no backend in Storybook, so it would
only ever show a spinner. Every other step keeps its production component by
spreading DEFAULT_STEP_COMPONENTS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Session-URL: <session URL>
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