Skip to content

test(proprietary): spend cap, policy banner and login link coverage - #7345

Draft
reecebrowne wants to merge 41 commits into
mainfrom
chore/storybook-proprietary-2
Draft

test(proprietary): spend cap, policy banner and login link coverage#7345
reecebrowne wants to merge 41 commits into
mainfrom
chore/storybook-proprietary-2

Conversation

@reecebrowne

@reecebrowne reecebrowne commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Stacked on #7324#7319#7318#7309. Merge those first.

Illustrated summary (all 20 stories rendered): https://claude.ai/code/artifact/cef8a4e1-4510-4317-b6d6-6dd56df9b051

What

Three proprietary components covered, and two corrections to the coverage report.

Component Stories
billing/SpendCapControl 8
components/viewer/PolicyEnforcementOverlay 8
routes/login/NavigationLink 4

The spend cap is driven by real state — choosing a preset, typing a custom amount, clearing back to no cap — plus the two cases where the document estimate can't be drawn: no per-document rate known, and no cap set. For the policy banner, which run it selects out of the list is the whole behaviour, so each in-flight status gets a story, along with a list where only one of several runs is live.

No defects — which is the finding

All three build on the design system's primitives. SpendCapControl's preset chips are Buttons with data-selected rather than styled divs; NavigationLink wraps Button. They inherit correct semantics for free.

Across this campaign the defects have clustered almost entirely in hand-rolled interactive markup — clickable divs, bare Progress elements, cards with mouse-only handlers. Component size and complexity didn't predict risk: SpendCapControl is 206 lines of custom control and came out clean. Reaching past the primitives is what predicts it. That's a useful filter for triaging the remaining 205 surfaces by risk rather than working through them in order.

Coverage report corrections

  • Watched folders excluded. Hidden and obsolete, so its components aren't gaps anyone should write stories for — they were reading as outstanding work.
  • Obsolete areas are a named list, not a bare regex, so the next one is a line and the reason sits where someone reading the report will look.

Verification

  • 20 new stories, 0 a11y violations.
  • Typecheck clean against the proprietary flavour specifically — @app/* resolves proprietary-first there, and its PolicyRunRecord has required fields core's doesn't. A story can render green in the scan while its fixtures are wrongly typed, so both checks matter.
  • task frontend:check:all — green; 1,778 tests pass (two known Windows-only path failures).

…st it exposed

The a11y baseline had grandfathered 839 story entries, 71% of them
color-contrast. Most of that was the harness lying: Storybook wrapped
every story in the Processor's Mantine theme (SuiProvider is mounted
only by PortalApp) and never loaded styles/theme.css, so the editor
theme's --color-* palette resolved to nothing and Mantine fell back to
its stock greys. Stories now get the theme their component runs under,
with the CSS vocab that theme needs.

That exposed the real failures underneath, all of which were colours
chosen as fills being reused as text:

- status accents: the -dark shades are the on-light text colours, so
  they now sit deep enough to clear 4.5:1 on the tinted surfaces they
  actually appear on (adds green-800, amber-700/800, red-700).
- filled accents: green and red fills darken so their white labels
  pass; amber keeps its hue and takes dark ink instead, because
  darkening amber far enough for white text turns it brown.
- new --c-accent-text for accent copy, which --c-primary and its hover
  mix were both too light for.
- form labels and the required asterisk were gray-400/base red (2.3:1
  and 3.4:1).
- Mantine's light variant derives its own label colour (~3.7:1); a
  css-variables resolver points each hue at the accessible shade.
- the theme defines red and aliases blue so components naming those
  colours stop falling through to Mantine's stock palette.

Also names Mantine's icon-only dismiss buttons, which had no
accessible text at all.
Mantine's c="blue"/"red"/"green"/"orange" resolve to the filled shade —
a background colour. As text on the page those land between 2.9:1 and
4.2:1. Point them at the theme's on-light text tokens, which are
contrast-checked and follow light/dark.
… name

Clears the structural half of the grandfathered baseline. Fixed at the
shared component wherever one existed, so the story files themselves
needed almost no edits:

- progressbars: the shared ProgressBar now REQUIRES a label, so an
  unnamed bar can no longer be written; StepIndicator names itself and
  the billing meters take a caller-supplied one.
- dialogs: passing aria-label to Mantine's Modal does nothing — rest
  props land on the outer wrapper and aria-labelledby is overwritten
  after the spread — so the three affected modals compose
  Modal.Root/Modal.Content instead.
- table headers: action and chevron columns get screen-reader-only text
  through the existing .sr-only utility.
- inputs: named mostly by pointing aria-labelledby at the heading text
  already on screen, so screen-reader users get the same words sighted
  users read; SettingsRow and FormField now wire this automatically.
- ARIA semantics: popover targets became real controls rather than divs
  wearing aria-haspopup, role=dialog moved off <aside>, chips stopped
  nesting a button inside a button, and the file grid's role=row now
  owns real cells.

Also drops an inline colour on the admin banner's link that was
overriding the themed accent.

18 new i18n keys, each added to both locales.
…d badges

The generated accent ramp mixed shade 600 to 88% of the base hue, which
lands at 4.2:1 on the page — and Mantine maps shade 6 to the link colour
and the tuple's default text, so every link and light-variant label
inherited it. Mixing further down clears it for any accent the user
picks; filled surfaces read from --mantine-primary-color-filled and are
untouched.

Also points the accent-as-text CSS at --c-accent-text rather than
--c-accent-fg (a fill/icon colour) in the seven places that used it, and
fixes the file-origin badges, which set their label to the raw accent
and Mantine's orange over their own 16% tints.
The filled warning kept its light amber and took dark ink, which was
the odd one out — every other filled accent is white on colour, and the
dark-on-amber pairing reads worse in practice than the contrast figure
suggests. Deepening the fill to amber-700 puts the white label at
5.02:1, matching success exactly and danger closely.

Also nudges the subtle-text grey: it was tuned to clear 4.5:1 on the
page canvas, but banners tint their background darker and it landed at
4.39:1 there.
The same fill-as-text substitution the tokens already fixed, now in the
75 component stylesheet declarations that reached past them: a bare
color: var(--c-primary) (or --c-danger/--c-success/--c-warning) reads
the fill value, which sits between 2.2:1 and 3.5:1 on the page. Each
now takes the matching text token; background, border and outline
declarations are untouched.

Also deepens the two code-palette greys, which are read on the code
header ground rather than on white — the muted one was at 2.6:1.
.storybook/ is where the scan and gate scripts live, and where eslint
expects Node globals.
…nd tokens

Sweeps the sources the earlier token work didn't reach: Mantine fill
variables used as text in stylesheets, inline style objects setting
color to an accent or status fill, base --color-* tokens (rather than
their -dark text counterparts), the primary light-variant text
variable, active tab labels, and two portal rules.
Clears the last 159 grandfathered violations, so the baseline is now
empty and any new violation fails the gate outright.

Three root causes accounted for nearly all of them:

- Mantine's semantic slots were never bound. Its -text, -outline,
  -light-color, -filled and -dimmed variables default to each hue's
  solid fill, which is chosen to carry white and is far too light to
  read as ink. Both theme resolvers now pin them to the -dark tokens.
  The portal theme had no such bindings at all, and its primaryColor
  tuple referenced --color-blue-*, which was never defined — so the
  portal silently fell back to stock Mantine blue. Those tokens now
  exist alongside the other SUI hues.

- The legacy --color-<hue>-50/100/200 rungs all pointed at saturated
  400-level primitives, so every "tint" background rendered as a fill.
  Real tints now back those rungs, which also repairs Mantine's [0..2]
  light-variant slots.

- Text faded with `opacity` rather than a muted colour. Seven sites
  dropped below the floor that way; each now recedes via ink or
  surface, which is what conveyed the state anyway.

Also names the icon-only assistant button, the ColorInput eye-dropper
and labelless loading buttons, and marks the disabled file-picker
trigger aria-disabled so axe stops treating it as an active control.

Adds --c-*-solid tokens for fills that must carry a white label, and a
data-user-content-preview opt-out for nodes that render a facsimile of
the user's own document in colours they chose.

Verified: 1,417 story renders, 0 violations.
Adds a shared story helper for tool settings panels. Most take a whole
parameters hook rather than a plain object, so a story needs the hook's
shape with state that actually updates when the panel writes to it —
otherwise every field freezes on a static snapshot.

The report's per-page list scrolls but holds no control of its own, so it
was unreachable without a mouse; it now takes a tab stop, matching the
file picker's scroll region.
The pipeline-builder form of both panels: a plain parameters object with
a change callback, rather than the tool's own hook.
The report's scroll region gained an accessible name; the key it uses
needs to exist in the locale.
Paged output preview: single and multiple results, thumbnails still
rendering, and the empty states.
…onButton

Tooltip reads the user's logo preference and the sidebar geometry it
positions against, so any story rendering one threw. It is used by
around a hundred components, which made this a harness gap rather than a
per-story one — both providers now sit alongside the others in preview,
matching how the real app always mounts them.

OperationButton is the run control every tool panel ends with; its
disabled states are the interesting ones, because each explains what the
user has to fix.
--c-accent-fg is the accent tuned as a foreground — it is used as a text
colour in fourteen places. Custom themes overwrite it from
--user-accent-fg, which is already forced dark enough on light bases, but
the default build left it on the raw fill at 3.67:1.

Caught by the new Badge story: none of its consumers had one, so the
token had never been scanned.
Wordmark, LogoIcon, SidebarToggleIcon and the app-switch menu items —
four small components that share the app's top-left corner, so they are
grouped in one file rather than four near-empty ones.

AppSwitchMenuItems is a fragment of dropdown items, not a standalone
menu, so the story mounts it in the Dropdown its callers use.
CardSelector's options were Cards with an onClick and nothing else — no
role, no tab stop, no key handler — so the split method (and every other
card choice built on it) could not be reached or chosen without a mouse.
They are controls, so they now say so, and a disabled option is marked
aria-disabled rather than merely faded.

Found by the new story: the component had none, so it had never been
scanned.
FileListItem's checkbox had onChange={() => {}} with a comment deferring
to the row's onClick — but that handler is mouse-only, so pressing Space
on the focused checkbox did nothing and file selection was unreachable by
keyboard. The checkbox now carries the keyboard path, and its click stops
at the wrapper so a mouse press doesn't toggle twice.
Two shapes of the same defect in checkout.

The plan cards already had a "Select Monthly" / "Select Yearly" button,
but it carried no handler — the click lived on the Paper around it, which
is mouse-only. Pressing Enter on the focused button did nothing. The
button is the natural control, so it now holds the handler and the card
stays a mouse convenience.

The billing-period cards in StaticCheckoutModal have no inner button at
all, so there the card really is the control and takes the semantics to
match, via a helper beside the style one it already used.
…al semantics

Three more click targets with no keyboard path.

DocumentThumbnail repeats its onClick across four render branches, so the
semantics are built once and only when a handler was actually given — a
decorative thumbnail should not take a tab stop.

The OCR setup-guide text was styled as a link and opened one, but was a
Text with an onClick: unreachable by keyboard and no target cue on hover.
It is now an anchor.

IconSelector's tiles are choices, so they say so, and each carries the
label its tooltip already showed.
Clicking the page backdrop clears the current selection, but that path is
mouse-only — a keyboard user had no way out of a selection at all. The
backdrop stays a plain surface rather than becoming a control; it is a
canvas, not a button.
Each thumbnail jumps the viewer to that page, but was a Box with an
onClick — so page navigation from the sidebar was mouse-only. They are
controls now, and aria-current marks the page being viewed, which the
highlight alone only conveyed visually.
…abel

The sidebar reads the viewer context for page counts and thumbnail
rendering. Rather than standing up a ViewerProvider and the document
pipeline behind it, the stories supply the small slice of context the
sidebar actually touches — enough to exercise page counts, the selected
page and the empty state.

The story immediately caught the current page's label sitting at 2.91:1:
it used the primary fill on the primary tint. It takes the accent text
step now, like the other accent copy.
Both files already had stories and I rewrote them wholesale, which
deleted ResultsPreview's Default and Loading and swapped
HoverActionMenu's typed HoverAction fixtures for ad-hoc icon imports.
Story ids are URLs, so removing a name breaks every link to it.

Restored both to what main has and layered the extra states on top.
The Advanced toggle carried its name only in a title attribute, which is
unreliable for screen readers and invisible to sighted keyboard users; it
takes an aria-label now. Caught by the new panel story.

HotkeyProvider pulls in the whole tool-workflow chain, but the display
reads one function off the context — so the context is exported and the
story supplies that slice, using the real formatter so the caps render as
they do in the app.
The frame every tour slide sits in: hero panel, copy, step position and
the action row — including the not-dismissable and disabled-action states,
which decide whether a user can get past a step.
Popover.Target applies aria-haspopup and aria-expanded to whatever it
wraps. Wrapping a plain div put aria-expanded somewhere it is not a
permitted attribute, so the disclosure state was invalid markup rather
than something assistive tech could use. The ActionIcon is the target now.

Caught by the new PageSelectByNumberButton story; the viewer's search
button had the same shape.
Counting a component covered by an adjacent .stories.tsx is wrong here —
MantineForms covers Select, MultiSelect, NumberInput and ColorInput
between them, and a sibling check reports those as gaps and invites
duplicate stories. Coverage is counted by import instead.

The useful part is the classification. A gap needing only props is
minutes of work; one needing a context slice or MSW handlers is not, and
the two should not sit in the same queue. Contexts the Storybook harness
already mounts do not count against a component, and modules that return
a config object rather than markup are not surfaces at all.
Storybook resolves @app/* through the proprietary vite tsconfig, but the
desktop flavour resolves it desktop-first, and 34 desktop files import
@app/* assets that exist only under desktop/. Those imports do not
resolve in Storybook, so a desktop story file fails to load outright —
worth knowing before someone spends an afternoon on it.
Not just desktop — saas, cloud and prototypes hit the same wall. Storybook
resolves @app/* through the proprietary vite tsconfig, so any flavour file
importing an @app/* asset that lives only in its own tree fails to resolve
and the story file never loads. That is what the 0% areas are, and the fix
is a build-config decision with blast radius across every existing story.
Both rail widths, since the mark swaps between icon-only and icon-plus-
wordmark as the sidebar collapses.
The classifier answers 'does this need a provider', not 'is this quick'.
ButtonAppearanceOverlay wants real PDF bytes and AppConfigModalLazy pulls
in the entire settings tree; both classify as props-only.
The viewer's *APIBridge components register an API into the viewer
context and render null — wiring, like the providers and gates already
excluded. Counting them made the viewer look worse covered than it is
and would have had someone writing stories that show a blank frame.
…overage report as a task

The shell already had stories and I replaced them wholesale, dropping
Default, SteppedWithBack and NotDismissible — and introducing a
NotDismissable spelling alongside main's NotDismissible. Story ids are
URLs, so each removal broke every link to it. Restored main's version and
layered the new states on top.

storybook-coverage.mjs was committed but referenced by nothing, so it was
undiscoverable; it now has a task alongside the other storybook ones.
@stirlingbot stirlingbot Bot added Front End Issues or pull requests related to front-end development Translation Issues or pull requests related to translation labels Aug 7, 2026
The context value type was unexported, so the fixture reached for `as
never` — which switches off checking entirely, meaning a change to the
context shape would not surface here. Exporting the type lets the fixture
be checked against the real thing.
…verlay

MeterBar's three states are the reason it exists, so each gets a story
rather than leaving the bar's fill to carry the meaning alone. The
overlay covers each progress case and the accent tones a policy can
carry.
A progressbar with values but no name announces a bare percentage — the
heading beside it is visual context a screen reader does not get from the
node. Eight were unnamed; each takes the wording already shown next to
it, reusing the existing key where one exists rather than inventing new
copy for the same string.

Verified per instance rather than by proximity: an earlier check looked at
a fixed window after the first <Progress> in each file, which would
mis-judge files holding several.
…ation link

The spend cap is driven by real state — presets, a custom amount and
clearing back to no cap — plus the cases where the document estimate
cannot be drawn. Which run the policy banner picks out of the list is its
whole behaviour, so each in-flight status gets a story.
The feature is hidden and obsolete and will not be unhidden, so its
components are not surfaces anyone should write stories for. Excluded
rather than left in the report, where they read as outstanding work.
A bare regex hid both what was excluded and why. A named list makes the
next obsolete area one line, and puts the reason where someone reading
the report will look.
@reecebrowne
reecebrowne force-pushed the chore/storybook-proprietary-2 branch from 99d0e16 to 2a0556a Compare August 7, 2026 23:26
@stirlingbot

stirlingbot Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🚀 V2 Auto-Deployment Complete!

🔗 Direct Test URL (non-SSL) http://54.175.155.236:7345

🧩 Admin portal included - try it at http://54.175.155.236:7345/portal.

📚 Storybook: http://54.175.155.236:27345/

30 stories changed (+3 config files)

Stories

  • core/components/hotkeys/HotkeyDisplay.stories.tsx
  • core/components/onboarding/OnboardingSlideShell.stories.tsx
  • core/components/pageEditor/BulkSelectionPanel.stories.tsx
  • core/components/pageEditor/PageSelectByNumberButton.stories.tsx
  • core/components/shared/AppSwitcher.stories.tsx
  • core/components/shared/Badge.stories.tsx
  • core/components/shared/BrandMarks.stories.tsx
  • core/components/shared/CardSelector.stories.tsx
  • core/components/shared/HoverActionMenu.stories.tsx
  • core/components/tools/addPageNumbers/AddPageNumbersAutomationSettings.stories.tsx
  • core/components/tools/addStamp/AddStampAutomationSettings.stories.tsx
  • core/components/tools/autoRotate/AutoRotateAutomationSettings.stories.tsx
  • core/components/tools/autoRotate/AutoRotateReport.stories.tsx
  • core/components/tools/autoRotate/AutoRotateSettings.stories.tsx
  • core/components/tools/shared/OperationButton.stories.tsx
  • core/components/tools/shared/ResultsPreview.stories.tsx
  • core/components/tools/shared/ToolWorkflowTitle.stories.tsx
  • core/components/viewer/ThumbnailSidebar.stories.tsx
  • core/components/viewer/nonpdf/MarkdownRenderer.stories.tsx
  • core/ui/ChatFABWindow.stories.tsx
  • core/ui/Forms.stories.tsx
  • core/ui/MantineForms.stories.tsx
  • core/ui/ProgressBar.stories.tsx
  • portal/components/ChatFABWidget.stories.tsx
  • portal/data/Ops.stories.tsx
  • proprietary/billing/MeterBar.stories.tsx
  • proprietary/billing/SpendCapControl.stories.tsx
  • proprietary/components/shared/PolicyEnforcingOverlay.stories.tsx
  • proprietary/components/viewer/PolicyEnforcementOverlay.stories.tsx
  • proprietary/routes/login/NavigationLink.stories.tsx

Config

  • .storybook/a11y-baseline.json
  • .storybook/a11y-triage.mjs
  • .storybook/preview.tsx

This deployment will be automatically cleaned up when the PR is closed.

🔄 Auto-deployed for approved V2 contributors.

@stirlingbot stirlingbot Bot added the has conflicts Pull request has merge conflicts with the base branch label Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Front End Issues or pull requests related to front-end development has conflicts Pull request has merge conflicts with the base branch Translation Issues or pull requests related to translation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant