Repository navigation
chore(frontend): add typography foundation for text, headings, controls - #1378
Conversation
…ale, 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 LeanerCloud#741 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t 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 LeanerCloud#741 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h 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 LeanerCloud#741 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
.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 LeanerCloud#741 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reached
This review includes 10 billable files and costs up to $2.50. Or wait 59 minutes for your next included review. View limit details
Comment |
Conflicts: - frontend/src/styles/components.css: button/.btn base rules took main's tokenized transitions (LeanerCloud#1366) with this branch's --cudly-fs-sm; .approval-details-table thead th took main's --cudly-surface-muted background (LeanerCloud#1382 replaced the undefined --cudly-surface-2) with this branch's --cudly-fw-semibold. - frontend/src/styles/modals.css: .archera-signup-btn and .archera-offer-signup keep this branch's --cudly-fs-md / --cudly-fw-medium and main's tokenized transitions. - frontend/src/styles/forms.css: main removed the duplicated ramp-option / #custom-ramp-config block (LeanerCloud#1366); took the removal. The surviving copy in plans.css already uses --cudly-fw-semibold for .ramp-label. - frontend/src/styles/tables.css: th took main's header style (LeanerCloud#1366), which already uses --cudly-fw-semibold; took main's removal of the .planned-purchases-table th override this branch had tokenized. - frontend/src/styles/tabs.css: took main's removal of the dead .tabs / .tab-btn rules (LeanerCloud#1366) that this branch had tokenized. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… merge
Main added .account-delete-pending-total { font-weight: 600 } (LeanerCloud#1373) and
.last-updated-stamp { font-size: 0.75rem } (LeanerCloud#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 LeanerCloud#741
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - frontend/src/styles/forms.css: both sides inserted text before .form-help. Kept main's .form-group [aria-invalid="true"] border rule (LeanerCloud#1368) and then this branch's comment and display: block on .form-help; the two rules are independent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): APPROVE Blocking:
|
…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>
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): APPROVE Blocking:
|
|
Local verification at
|
|
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 |
Brings in LeanerCloud#1365, LeanerCloud#1381, LeanerCloud#1378 and LeanerCloud#1389. No conflicts; index.html auto-merged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts: - frontend/src/styles/components.css: main (LeanerCloud#1378 typography tokens) only swapped literal font weights for --cudly-fw-* tokens inside the .status-badge and .badge-admin/-user/-readonly rules, which this branch removes; kept the removal. In .badge kept this branch's line-height: var(--cudly-lh-tight) and white-space: nowrap together with main's tokenized font weight and radius. - frontend/src/styles/tables.css: main only tokenized the font weight of the planned-purchases Bootstrap badge palette, which this branch replaces with the shared .badge tones; kept this branch's .planned-purchase-row.status-paused dimming rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Main's typography foundation (LeanerCloud#1378) forbids literal font weights that duplicate a --cudly-fw-* token; the ladder config form's labels and section divider still used 500 and 600. Refs LeanerCloud#742 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(frontend): name ladder accounts and guard Auto Approve saves The ladder config table listed each configuration by its internal account UUID, Auto Approve could be saved without any hint that later ladder purchases skip human approval, and a rejected save replaced the server's validation message with a generic failure. The table now loads the registered account list alongside the configs and shows "name (external ID)", keeping the UUID as the cell title and falling back to it when the list is unavailable. Selecting Auto Approve shows an inline warning, and a save that newly turns on unattended purchasing (enabled + Auto Approve) asks for confirmation first, the same pattern the RI exchange Fully Automated mode uses. Save failures now show the API's error text. The table renderer moves to ladder-config-table.ts to keep ladder.ts under 500 lines. Refs #742 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(frontend): move ladder config modal markup to its own module ladder.ts sits at 494 lines, just under the 500-line rule, and the config modal's form layout needs reworking (review feedback: huge inputs and word-wrapped labels). Moving the static markup out first keeps that change reviewable and leaves ladder.ts room. No markup changes. Refs #742 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * style(frontend): compact two-column layout for the ladder config modal Review feedback: the Add/Edit Account Config form laid each label and input side by side, so long labels wrapped word by word, every input stretched to the wrapped label's height, input edges were ragged, the Ramp Schedule textarea was squeezed next to its help text, and on short viewports the title scrolled away with the form. Labels now sit above standard-height inputs, and the short fields pair up in an aligned two-column grid (one column under 640px). Range and unit qualifiers such as "Minimum 10" or "Leave blank for no cap" move to muted hints under the inputs, linked with aria-describedby. The ramp textarea spans the full width with its help below. The header stays fixed while the body scrolls, with Cancel / Save pinned to the bottom. Field IDs, validation and save behaviour are unchanged. Refs #742 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(frontend): dismiss the validation toast before re-saving in the ladder e2e test Main's cleared-Buffer-Fraction test (#1326) clicks Save again while the validation toast is up. With this branch's compact modal the Save button is pinned to the bottom-right, under the toast stack at the default 1280x720 viewport, so the click was intercepted. Dismiss the toast first, as ladder-config.spec.ts already does between saves. Refs #742 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(frontend): keep the toast stack off an open dialog's Save button At 1280x720 the compact ladder config modal puts Save in its bottom-right corner, directly under the bottom-right toast stack. A validation or server-error toast stays up for 30 seconds and swallowed clicks on Save until it was dismissed. While a dialog box (.modal-content or .modal-confirm) is on screen, toast.ts now lays the stack out around it: beside the dialog, narrowed to the gap on its right, when that gap is at least 240px wide (268px at 1280x720 for the ladder modal), otherwise lifted above the dialog's button row (.modal-footer, .modal-buttons, .modal-confirm-actions). The positions are CSS custom properties set through the CSSOM, so no style attributes are added (CSP). A MutationObserver and a resize listener re-run the layout while toasts are showing, so a toast that was already up when a dialog opens moves too; nothing is watched while the stack is empty. This applies to every dialog, not only the ladder modal. The ladder e2e test no longer dismisses the validation toast before saving again; it asserts the toast does not cover Save. A new e2e test at 1280x720 and 390x844 shows a server-error toast and clicks Save again with it still up. Without this change those three e2e checks fail with the toast's box over Save's. Refs #742 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(frontend): declare the toast stack's position properties css-custom-properties.test.ts (#745) requires every var() a stylesheet reads to be defined. Declare --toast-bottom and --toast-max-width on .toast-container with the old 1rem / 400px values and read them without fallbacks; toast.ts's inline override still wins while a dialog is open, and removing it restores the defaults. Refs #742 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(frontend): move the toast stack only when it would cover a dialog's buttons The stack was lifted above an open dialog's button row whenever the gap beside the dialog was under 240px, without checking that the toast's corner touched that row. A short centred .modal-wide dialog at 1280x720, or a confirm stacked over the ladder modal, had the toast moved from empty space onto the dialog's content, including a destructive confirm's warning. layoutToastStack() now measures the stack at its default corner and moves it only when that box overlaps an on-screen button row of the topmost dialog (a confirm over a .modal; rows picked by visible rect, not DOM order). It then goes beside the dialog (width capped at 400px), above the whole dialog when there is room, or just above the covered row. Layout also re-runs on scroll inside a dialog, and the watchers are torn down once the last toast is gone. The CSS comment no longer claims more than that. Refs #742 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(frontend): check toasts stay off short and stacked dialogs Browser coverage for the toast placement fix: at 1280x720 an error toast raised over the short, centred account overrides dialog (.modal-wide) stays in its corner instead of moving onto the dialog, and at 1100x720 a toast that is up when the Auto Approve confirm opens over the ladder modal stays off the confirm and its warning text. Both fail on the previous layout code. Refs #742 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(frontend): use the font-weight tokens in the ladder form styles Main's typography foundation (#1378) forbids literal font weights that duplicate a --cudly-fw-* token; the ladder config form's labels and section divider still used 500 and 600. Refs #742 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…tatus Main's typography foundation (LeanerCloud#1378) forbids literal font weights in modals.css that duplicate a token. The fan-out status line used a literal 600; use var(--cudly-fw-semibold) instead, same weight. Refs LeanerCloud#757 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#1390) * fix(frontend): keep the bulk purchase modal open after a partial failure and retry only failed buckets When some buckets of a multi-bucket ("fan-out") purchase failed, the modal closed and the selection was cleared before the outcome was classified, and the toast truncated the reasons (3) and labels (5). Users had to find and reselect the failed rows in a large table, at the risk of resubmitting the buckets that had already gone through. On any bucket failure the modal now stays open: - each bucket section shows "Submitted: awaiting approval [from X]" / "Submitted: joined existing request ..." or "Failed: <full reason>"; - submitted buckets are dimmed and their payment selects are locked; - a banner (role=alert) states how many were submitted and failed, that submitted buckets will not be sent again, and that closing the dialog does not cancel them; - the fan-out state is narrowed to the failed buckets, so the submit button becomes "Retry N failed" and resubmits only those. A failed bucket's payment can still be changed before retrying. The sticky failure toast is skipped in this mode (the modal has the full detail, and the toast covered the Retry button), and the Archera offer waits until the purchase modal closes. A fully successful retry closes the modal as before. Buckets get a stable id (copies from getFanOutBuckets() lose identity) and sections carry data-bucket-id so results map back to their section. Callers without ids keep the previous close-and-toast behavior. Refs #757 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(frontend): cover partial bulk purchase failure and retry Jest: the modal stays open with per-bucket status, submitted buckets are locked, the button reads "Retry N failed", a retry posts only the failed buckets and closes the modal (and opens the Archera offer) once all succeed, a second partial failure keeps earlier successes counted, an all-failed run also stays open, buckets without ids keep the old behavior, and failure reasons cannot inject markup. Playwright: three providers, Azure fails once, the dialog shows the failure, Retry 1 failed posts only the Azure bucket and closes. Refs #757 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(frontend): match the bulk-approval confirm label in the fan-out retry e2e Main's #1362 renamed the fan-out confirm button from "Send all for approval" to "Send for Approval ($X upfront on approval)", naming the combined upfront of the buckets being sent. The partial-failure spec still clicked the old label and timed out. It now matches the new label with the amount as a pattern, since the first send and the retry carry different totals. Refs #757 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(frontend): use the semibold weight token for the fan-out bucket status Main's typography foundation (#1378) forbids literal font weights in modals.css that duplicate a token. The fan-out status line used a literal 600; use var(--cudly-fw-semibold) instead, same weight. Refs #757 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(frontend): only enter fan-out retry mode while the submitting modal is open showFanOutRetry only checked that #purchase-details still held fan-out sections. Closing the modal (Escape or Cancel) while the POSTs were in flight leaves those sections in the hidden modal, so a failed bucket put its banner there and suppressed the failure toast: the user got no notice at all. Bucket ids were also per-index and reused on every open, so results settling after the user opened a new bulk modal marked its sections submitted and narrowed its buckets to the old failed ids. Bucket ids now carry the purchase-modal open token, and retry mode is entered only when #purchase-modal is visible and still holds the submitted bucket ids. Otherwise the toasts fire as on main. If a newer purchase modal is open, it is not closed or cleared. The #1503 payment-adjustment toast also uses the default timeout in retry mode instead of staying on top of the Retry button. Refs #757 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The stylesheet had no body line-height (text used the browser's cramped ~1.2 default), form controls did not inherit the page font (inputs in one form rendered at 13.33px, 14.4px and 14px), and headings without a page-specific rule got browser default sizes. This PR adds a body line-height, a token-based heading scale and one font size for form controls, and swaps literal sizes and weights that exactly equal a design token for that token.
Refs #741
Partial: off-scale literal font sizes (0.8rem, 0.85rem, 0.9rem, 0.95rem, ...) are not snapped onto the token scale, because that visibly changes dozens of components and needs a design call.
What changed
frontend/src/styles/base.css:body { line-height: var(--cudly-lh-normal) }(1.5);button, input, select, textarea { font: inherit; font-size: var(--cudly-fs-sm); line-height: var(--cudly-lh-tight) }; a zero-specificity:where(h1..h6)scale on the tokens (h1--cudly-fs-2xl, h2xl, h3lg, h4md, h5/h6sm) with tight line-height and bold weight (3e46a9a).--cudly-fs-*value (0.75/0.875/1/1.75rem) and all font weights (bold, 700, 600, 500) now use the matching token, about 60 lines across 6 CSS files (components, forms, modals, plans, settings, tables). Pure substitution: no computed value changes (751ea48)..modal-contentinput rule and the bareselectrule use--cudly-fs-sm(14px) instead of 14.4px, so every control in a form matches the buttons (73385ea)..account-delete-pending-totalfont weight 600 and.last-updated-stampfont size 0.75rem (25c2918). No computed value changes..form-help(<small>) is nowdisplay: block, placed after main's.form-group [aria-invalid="true"]rule. As an inline element its wrapped lines sat 24px apart under the new line-height and itsmargin-topnever applied (b772276).frontend/src/__tests__/typography.test.ts(jest, reads the stylesheets) andfrontend/tests-e2e/typography.spec.ts(Playwright, checks computed fonts in Chromium, which jsdom cannot cascade).Why this approach
The heading scale uses
:where()so the ~20 existing contextual heading rules keep winning; only headings that had no rule change size. The scale tracks the browser defaults it replaces, and heading weight stays bold so headings that set only a size do not change weight. Form controls keep a tight line-height so button and input heights barely change (otherwise every button would grow about 4px). Only exact-equivalent literal-to-token swaps are included; snapping off-scale sizes would be a visible design change and would collide with most other open CSS branches.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.
Verification
Run on this branch's head
f5ba4e5d, merged with main6122fa24:npm run lint: 0 errors. Typecheck and build pass.typography.test.ts(20 tests over 13 stylesheets).typography.spec.tstests. The input/button size check fails on main.components.css,modals.css,forms.css,tables.cssandtabs.cssresolved, see the merge commits.Notes for review
.modal-content labelisdisplay: blockand the checkbox getswidth: 100%from the modal input rules. That belongs to a forms/modal issue.typography.test.tsguard against literals that duplicate a token only matchesfont-size: <literal>;andfont-weight: <literal>;. It does not catch!important, thefont:shorthand or px equivalents..form-helpkeeps its off-scale 0.8rem size.Overlaps
This touches
base.css,components.css,forms.css,modals.css,plans.css,settings.cssandtables.css. Open CSS branches that edit the same files: #737 (status badges; conflicts textually) and #743 (modal/card unification; merges cleanly with this branch today).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