Skip to content

fix(frontend): card layout and UTC date inputs for ladder purchases - #1387

Merged
IAmJSD merged 3 commits into
LeanerCloud:mainfrom
IAmJSD:ui/issue-753
Oct 24, 2026
Merged

IAmJSD merged 3 commits into
LeanerCloud:mainfrom
IAmJSD:ui/issue-753

Conversation

@IAmJSD

@IAmJSD IAmJSD commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

The ladder "Planned purchases" block sat flush under the "Add Account Config" button inside the laddering card, with large body-text labels glued to their controls and four plain paragraphs stacked with no hierarchy. Its From/To filters were datetime-local pickers that display in the browser locale (e.g. "10/24/2026, 09:15:00 AM") under a "(UTC)" label. This PR renders the block as its own settings card with labelled form groups, a stat line and muted footnotes, and replaces the date pickers with ISO UTC text inputs.

Closes #753

What changed

  • frontend/src/ladder-timeline.ts:
    • The block is its own fieldset.settings-category card with the legend "Planned purchases". The intro text is .settings-help.
    • Each control is a .form-group whose <label for> sits above it; Reload timeline is .btn-secondary; Clear filters is .btn-tertiary (link style).
    • The totals line is .ladder-timeline-stat (bold). The run snapshot, the date hint and the "Service, instance type ... unavailable" caveat are muted .ladder-timeline-note footnotes, and the caveat moved below the table.
    • From/To are text inputs with placeholder YYYY-MM-DD HH:MM and the hint "Enter dates in UTC as YYYY-MM-DD HH:MM (24-hour); the time is optional." Valid input is shown back in ISO form. Invalid input sets aria-invalid, shows a role="alert" .field-error, and keeps the last valid filter; Clear filters clears the error.
  • frontend/src/ladder-timeline-filters.ts: new parseLadderUtcInput/formatLadderUtcInput. They accept YYYY-MM-DD optionally with HH:MM or HH:MM:SS, a T or space separator, and a trailing Z/UTC, and reject impossible dates by round-tripping the value. A bare "to" date covers through 23:59:59.
  • frontend/src/ladder.ts: moves the #ladder-purchase-timeline div out of the laddering fieldset (3 lines) so the two cards don't nest. It stays inside #commitment-laddering-settings, so the Purchasing "Laddering" nav link and main's hidden-section guard in ladder-timeline.ts still cover it.
  • frontend/src/styles/charts.css: replaces the old label grid rule with filter-row, 12px muted label, stat and note rules built on the design tokens.
  • Tests: ladder-timeline-filters.test.ts, ladder-timeline.test.ts, and new tests-e2e/ladder-timeline-layout.spec.ts.

Why this approach

No browser lets a page control the datetime-local display format (Chromium ignores lang and the page locale and uses the OS locale), so showing and accepting the ISO string is the only way to make UTC unambiguous. The cost is the native picker; in exchange, typing and pasting are exact and match the "Scheduled (UTC)" column. The card and .form-group pattern copies the other Admin settings cards, as the issue suggests, instead of adding new components. The caveat moved to a footnote so the totals line sits directly above the chart it describes.

Screenshots

Screenshots were taken before this branch was merged with main; before = main at a14dc8f. After is this branch at that time. Both are headless Chromium at 1440x900 against the repo's mocked e2e API (the narrow pair is at 390px wide).

Before After
planned-purchases before planned-purchases after
planned-purchases-narrow before planned-purchases-narrow after
  • 1440px: on main, "Planned purchases" is glued under "Add Account Config" inside the laddering card, with big body-text labels, From (UTC) showing "10/24/2026, 09:15:00 AM", and four plain stacked paragraphs. After: its own card with a legend, small muted labels above the controls, ISO UTC date inputs with a format hint, Clear filters as a link button, and a bold totals line above the chart.
  • 390px: on main, the same unstyled stack with locale date pickers. After: the card and label-above-control groups wrap cleanly with no horizontal overflow.

Verification

Run on this branch's head a06f34c5, merged with main 6122fa24:

  • Lint: 0 errors. Typecheck and build pass.
  • Jest: 4099 passed, 1 skipped, 0 failed. New tests: ladder-timeline-filters.test.ts covers the parse/format rules (whole-day edges; rejects 02.01.2030, 01/02/2030, 2030-02-30, 24:00 and 8:30). ladder-timeline.test.ts adds 2 tests: the card and legend render, every control has a labelled .form-group, there is no datetime-local input and the default From is ISO; valid To dates filter and normalize while an invalid one is flagged and ignored until Clear filters.
  • Playwright via e2e-local.sh: 233 passed, 0 failed. New ladder-timeline-layout.spec.ts at 1280px and 390px checks that the card is separate from the laddering card, labels sit above controls at 12px, typed dates filter the summary, an invalid date shows the alert and changes nothing, Clear filters resets, and the card doesn't overflow horizontally.
  • Secrets scan clean.
  • Merged with main at 6122fa2; conflicts in ladder-timeline.test.ts resolved, see the merge commits.

Notes for review

  • Main UX trade-off: the native date/time picker is replaced by ISO text inputs. Keeping the picker and adding an ISO readout next to it is the alternative, but the picker would still show locale/AM-PM text.
  • Clear filters uses the existing .btn-tertiary link style as the "ghost" button.
  • Copy: the hint text and the error 'To date "..." is not a valid UTC date. Use YYYY-MM-DD HH:MM.'
  • The status line ("Timeline loaded. All amounts are planning budgets.") stays body-size because it is the live status; only the other paragraphs were demoted.
  • With two invalid dates the error message can go stale: if From is still invalid after To is fixed, the alert keeps naming To.
  • The single-event editor modal (ladder-timeline-editor.ts) still uses a datetime-local input for the scheduled date. It is outside this block and could get the same treatment later.

Overlaps

Measured with git merge-tree:

Note for the UI owner

This changes UI code that @cristim owns. IAmJSD reviewed the before/after screenshots and asked for it to be merged; any design point you disagree with can be a follow-up.

🤖 Generated with Claude Code

…te inputs

The planned-purchases block rendered inside the laddering card with no
section gap, body-size labels glued to their controls, four unstyled
paragraphs stacked without hierarchy, and datetime-local inputs that
display in the browser locale (with AM/PM) under a "(UTC)" label.

The block now renders as its own settings card with a legend. Each
filter is a .form-group with a 12px muted label above the control; the
totals are one bold stat line, the run snapshot and the missing-fields
caveat are muted footnotes, and Clear filters is a tertiary button. The
From/To filters are text inputs that take UTC as YYYY-MM-DD HH:MM
(time optional; a bare "to" date covers the whole day), are shown back
in the same ISO form, and flag an unparseable value inline instead of
guessing.

Refs LeanerCloud#753

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD IAmJSD added priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged urgency/this-quarter Within the quarter impact/few Limited audience effort/s Hours type/bug Defect labels Oct 24, 2026
@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Caution

Review failed

The pull request is closed.

Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

IAmJSD and others added 2 commits October 24, 2026 16:52
Conflicts:
- frontend/src/__tests__/ladder-timeline.test.ts: both sides appended a
  describe block at the end of the file. Kept both: this branch's
  "planned purchases layout and UTC date filters (issue LeanerCloud#753)" followed
  by main's "chart teardown on navigation (issue LeanerCloud#667)" (LeanerCloud#1357).

ladder-timeline.ts merged cleanly: main's disposeLadderTimeline and the
hidden-section guard sit before this branch's card template. The moved
#ladder-purchase-timeline container is still inside the Purchasing
panel, so the root.closest('.hidden') guard keeps working.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): APPROVE a06f34c51aa3c7b35a53176ed0b8ad9a0540a222

Blocking:

  • none
    Non-blocking:
  • frontend/src/ladder-timeline.ts:179-183: with two invalid dates the error text goes stale. Scenario: type a bad From (the message names From), then a bad To (the message now names To), then fix To. Because From is still aria-invalid, the error is not cleared or rewritten, so the alert still says To date "…" is not a valid UTC date while To shows a valid normalized value and the real offender is From. Rebuild the message from whichever inputs are still invalid.
  • frontend/src/ladder-timeline.ts:53: role="alert" with identical repeated text (re-entering the same bad value) may not be re-announced by some screen readers, because the textContent doesn't change. This is minor.
  • The editor modal (ladder-timeline-editor.ts) still uses datetime-local for its scheduled date, so the same block now uses two date-entry formats. The body already acknowledges this.
    Attacked: Read git diff origin/main...HEAD in full. Both merges (4420923 and a06f34c) diff against their main parent to exactly the branch's 7 files. Main's fix(ladder): timeline editor 409 recovery, focus and chart cleanup #667 (fix(ladder): dispose the timeline chart when leaving its section #1357) disposeLadderTimeline, the root.closest('.hidden') guard and its "chart teardown on navigation" describe block are intact. The conflicted test file keeps both describe blocks verbatim. The moved #ladder-purchase-timeline div is still inside #commitment-laddering-settings, so feat(frontend): Admin Purchasing policies page is very long with a mid-page partial load #190's "Laddering" nav anchor and the hidden-panel guard still cover it. Checked XSS: the template only interpolates static labels, and the error uses textContent with the user's input. Every new control has a label for and an id. Checked the parser: round-trip rejects 02-30, 13-01, 24:00 and 8:30, and a bare "to" date covers through 23:59:59. Main's design tokens used in charts.css exist. Existing tests that type ISO T values still pass. f2ab673 merges cleanly. Cross-checked with merge-tree: 753 vs 742 and 753 vs 744 are clean. Ran jest (ladder-timeline*, ladder, css*) in a scratch worktree: 299/299.

@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Local verification at a06f34c5 merged onto current main (49de8f86; merge-tree 04af8e9b; Linux, Node, headless Chromium):

  • This branch's own checks passed at this head: lint, typecheck, build, the full jest suite, the full Playwright e2e suite, and the git-secrets checks (see Verification in the PR body).
  • The combined tree of every UI PR in this merge wave (tree 04af8e9b, main plus 5 PRs) passes npm ci, lint, typecheck, build, jest (1 skipped, 4122 passed, 4123 total), and the full Playwright suite (238 passed).
  • CI is green on a06f34c5 (CI Success, Run pre-commit hooks).
  • The independent review above approves this exact head.
  • Nothing was applied to any cloud.

@IAmJSD
IAmJSD marked this pull request as ready for review October 24, 2026 20:58
@IAmJSD
IAmJSD merged commit 19534fc into LeanerCloud:main Oct 24, 2026
29 checks passed
@IAmJSD

IAmJSD commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator Author

Marked ready and merged by IAmJSD's agent, at IAmJSD's request (IAmJSD reviewed the before/after screenshots; cristim is traveling), after the independent review and local verification above, all at a06f34c5.

IAmJSD added a commit to IAmJSD/cloud-commitments-platform that referenced this pull request Oct 24, 2026
No conflicts (picks up LeanerCloud#1387, which main gained after the previous merge).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IAmJSD added a commit to IAmJSD/cloud-commitments-platform that referenced this pull request Oct 24, 2026
…n added

Main's LeanerCloud#1387 added .ladder-timeline-controls label (font-weight 500)
and .ladder-timeline-stat (font-weight 600) to charts.css. Swap them for
--cudly-fw-medium and --cudly-fw-semibold; computed values are unchanged.

Refs LeanerCloud#741

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IAmJSD added a commit to IAmJSD/cloud-commitments-platform that referenced this pull request Oct 24, 2026
No conflicts: main's LeanerCloud#1387 (ladder timeline) merged cleanly and touches no file this branch changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IAmJSD added a commit to IAmJSD/cloud-commitments-platform that referenced this pull request Oct 24, 2026
Conflicts:
- frontend/src/history.ts: utils import. Main (LeanerCloud#1376) added NO_VALUE to
  the utils import; this branch added the historyConfirmBody import. Kept
  both.
- frontend/src/__tests__/ladder-timeline.test.ts: both sides appended a
  test at the same spot. Kept this branch's refreshLadderTimeline test
  (LeanerCloud#728) and main's planned-purchases layout and UTC date filter describe
  block (LeanerCloud#1387, issue LeanerCloud#753) one after the other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
IAmJSD added a commit that referenced this pull request Oct 24, 2026
…ls (#1378)

* chore(frontend): add a typography foundation (line-height, heading scale, control font)

The body had no line-height, so text used the browser's ~1.2 default.
Form controls did not inherit the page font, so .form-group inputs
rendered at 13.33px system text next to 14px buttons. Headings had no
base scale: every heading without a contextual rule got the browser's
em-based default.

- body: line-height var(--cudly-lh-normal) (1.5).
- button, input, select, textarea: font: inherit, size pinned to
  --cudly-fs-sm (the button size) and line-height --cudly-lh-tight, so
  control heights stay where they were. Element selectors keep the
  existing `select`/`button`/class rules in charge.
- :where(h1..h6): a 5-step scale on the --cudly-fs-* tokens (2xl, xl,
  lg, md, sm), tight line-height, bold weight. Zero specificity, so the
  ~20 contextual heading rules still win.

Refs #741

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* refactor(frontend): use the type tokens where a literal size or weight equals one

Literal font sizes that are exactly a --cudly-fs-* value (0.75rem,
0.875rem, 1rem, 1.75rem) and every numeric or keyword font weight (bold,
700, 600, 500) now use the matching token, so a later change to the
scale reaches them. Pure substitution: every computed value is
unchanged. Off-scale sizes (0.8rem, 0.85rem, 0.9rem, ...) are left as
they are, since snapping them to the scale is a visible design change.

Refs #741

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(frontend): give form controls one size and pin the foundation with tests

Inside the Create API Key modal the text input got 14.4px from the
.modal-content rule while the password and date inputs fell back to the
browser's 13.33px, next to 14px buttons. The modal input rule and the
bare `select` rule now use --cudly-fs-sm, so every control in a form
matches the buttons (a 0.4px change for those two rules).

Tests: a jest suite reads the stylesheets for the body line-height, the
control font rule, the heading scale and the absence of token-duplicate
literals; a Playwright spec checks the computed input/button font and
body line-height in Chromium, which jsdom cannot cascade.

Refs #741

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(frontend): render form help text as a block

.form-help is used on <small> elements, which are inline by default.
With the new 1.5 body line-height their wrapped lines sat on the 16px
parent line box and ended up 24px apart, and their margin-top never
applied. As a block they get their own 0.8rem line box and the intended
gap below the input.

Refs #741

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(frontend): tokenize the font size and weight main added after the merge

Main added .account-delete-pending-total { font-weight: 600 } (#1373) and
.last-updated-stamp { font-size: 0.75rem } (#1314). Both equal a design
token exactly, so the typography test that pins 'no literal that
duplicates a token' failed after merging main. They now use
--cudly-fw-semibold and --cudly-fs-xs; computed values are unchanged.

Refs #741

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(frontend): use typography tokens in the ladder timeline rules main added

Main's #1387 added .ladder-timeline-controls label (font-weight 500)
and .ladder-timeline-stat (font-weight 600) to charts.css. Swap them for
--cudly-fw-medium and --cudly-fw-semibold; computed values are unchanged.

Refs #741

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
IAmJSD added a commit to IAmJSD/cloud-commitments-platform that referenced this pull request Oct 24, 2026
No textual conflicts. frontend/src/ladder.ts and
frontend/src/styles/components.css auto-merged: main's LeanerCloud#1387 moved the
#ladder-purchase-timeline container out of the laddering fieldset and
LeanerCloud#1378 swapped literal font sizes/weights in components.css for the
--cudly-fs-*/--cudly-fw-* tokens; this branch's ladder table/modal split
and toast stack variables sit in other hunks and are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/few Limited audience priority/p2 Backlog-worthy severity/medium Moderate harm triaged Item has been triaged type/bug Defect urgency/this-quarter Within the quarter

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(frontend): ladder planned-purchases block has no layout styling and locale date inputs

1 participant