Skip to content

UN-4361 [FEAT] Add v1 toasts, inline alerts, progress and skeletons behind one notification entry point - #2426

Merged
johnyrahul merged 46 commits into
mainfrom
UN-4361-feedback
Nov 5, 2026
Merged

johnyrahul merged 46 commits into
mainfrom
UN-4361-feedback

Conversation

@johnyrahul

@johnyrahul johnyrahul commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Johny · project thread

What

  • Frontend v1 feedback components (P1-T13), in frontend/src/v1/components/ui/feedback/:
    • Toaster: the single toast region, on Radix Toast. Success, error, warning and info, with an optional action and an optional persistent flag. Toasts stack bottom-right, three at most; timed toasts drop before persistent ones. They pause while hovered or focused, and F8 moves focus to them. Reusing an id replaces a toast with a fresh one, which gets a new timer and is announced again. A requestId is shown and can be copied. Swipe-to-dismiss is off.
    • InlineAlert: the error state from dev spec section 8, "inline, above the content it concerns, with a retry".
    • ProgressBar (Radix Progress, determinate and indeterminate, name required; a bad max or value gives an empty bar) and StepProgress (done, current, upcoming; aria-current="step").
    • Skeleton with SkeletonText (12 then 10 tall, widths 100%, 78%, 46%), SkeletonBlock, SkeletonAvatar, SkeletonTableRows.
    • Spinner: button and inline sizes only, so it cannot become a page loader.
  • lib/notify.ts: the notification contract. notify() and toast.*; the Toaster registers itself with setNotifier. With nothing mounted, the message is only logged to the console with its request id; it is not shown or queued.
  • lint:v1: check-v1.mjs rejects sonner, react-hot-toast, react-toastify and notistack outside components/ui/feedback/. There, radix-ui is allowed only as a static named import that does not name Toast; any other reference to it is reported. antd is already rejected by Biome. 12 guardrail fixtures.
  • README: a Feedback section and the lint:v1 row.

Why

  • P1-T13 (UN-4361): "one way to tell the user something happened". It gates P1-T11 Data table, P1-T14 Screen-state kit and P1-T18 File inputs.

How

Decision Why
Contract lives in lib/, not in the component services/ may import only lib/, and P0-T05's error normaliser is meant to call it. Proposed to Kirtiman on UN-4313.
Radix Toast, not sonner v0's global sonner CSS reaches v1 (listed in the v1 README). Radix gives pause on hover and focus, the F8 viewport hotkey and per-toast assertive/polite announcements.
Toasts keyed by a per-show() revision, not by id A replacement must remount: its own timer, persistent and announcement. An action that re-notifies its own id must not be removed by its own close.
Provider remounts when the last toast closes Radix leaves its hover pause set when the viewport empties under the pointer, so the next toast would never time out.
Errors announce assertively (type="foreground"), everything else politely Spec: "error toasts are assertive".
Lint rule is structural (only named, non-Toast radix-ui imports) There are too many ways to spell a path to Toast to chase them one by one.
Skeleton region has no aria-busy On a live region it holds announcements until it turns false, and a skeleton unmounts instead, so the label would never be read.
Markup snapshot is __snapshots__/markup.md via toMatchFileSnapshot typecheck:v1 rejects .snap files under v1.
Tests use plain matchers src/v1/tsconfig.json has no jest-dom types; adding them belongs to P0-T08's harness.
ui:box-border on padded full-width boxes v1 ships no preflight, so w-full plus padding overflowed. In the browser, toasts ran off the screen edge.
Stand-ins for missing tokens No info token, so info uses primary. No destructive-muted token, so the InlineAlert fill is muted, as in frame 622:1320. Skeleton radius 4 and bar colour #e4e4e7 are not tokens, so they use rounded-sm and muted. Raised with design.

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • No. Everything new is under frontend/src/v1/, which no v0 route renders, plus a v1-only lint rule and its fixtures. No v0 file or dependency changes.

Relevant Docs

  • P1-T13 spec. Frames: 622:1748 and 1475:1419 (toasts), 622:1320 (alert), 757:310 (progress), 676:9632 (skeleton), dev-spec section 8.

Related Issues or PRs

Dependencies Versions / Env Variables

  • None. radix-ui, lucide-react and class-variance-authority are already dependencies.

Notes on Testing

  • bun run typecheck:v1, bun run lint:v1 and biome ci src are clean. bun run test:v1 passes 73 vitest tests and 101 guardrail fixture rows, and the full frontend vitest suite passes 728 tests.
  • Toaster tests cover:
    • timeout, persistent and pause on hover
    • the action, called with no arguments
    • dismissal by the close button, by id and with Escape
    • F8 keyboard reach
    • the cap, with timed toasts dropped before persistent ones
    • replacement: a fresh timer, persistence and announcement
    • a replacement sent by the toast's own action
    • focus moving to the viewport when a focused toast is removed
    • the hover pause after the last toast closes
    • the request id
    • a container change
    • assertive vs polite announcement for every kind
    • the unmount fallback and the portal container
  • New behaviour was checked against mutants: each revert fails at least one test.
  • Rendered in Chromium in Light and Dark through a throwaway Vite page (screenshots are in the project thread).

Screenshots

The Light and Dark renders are attached in the project thread. v1 has no route yet, so there is no in-app page to visit.

Checklist

I have read and understood the Contribution Guidelines.


Generated by Claude Code

jaseemjaskp and others added 30 commits October 26, 2026 17:51
…N-4270)

Adds frontend/src/v1/ as an isolated tree for the v1 frontend (P0-T01):

- Folder taxonomy from the spec, a public entry (src/v1/index.ts, imported
  by v0 as `@v1`) and a cloud overlay placeholder.
- src/v1/tsconfig.json: strict, no JS, unused locals/params are errors,
  `@v1` alias only. v0's tsconfig/jsconfig exclude src/v1.
- Biome overrides for the two-way import boundary and v1 bans (no `../`,
  `@/`, prop-types, antd, component CSS; noExplicitAny).
- scripts/check-v1.mjs for what Biome cannot express: TypeScript-only
  files, the folder taxonomy and import contract, PropTypes, `ant-`.
- typecheck:v1, lint:v1 and test:v1 scripts; v0 scripts are unchanged.
- test-fixtures/v1-boundary/ proves each rule fails and passes as
  specified, run with node --test so the v0 vitest suite never sees it.
- .gitignore re-includes src/v1/lib/ (the Python `lib/` rule matched it).

v0 build output, test counts, typecheck and Biome results are unchanged.
…270)

check-v1.mjs:
- Scan comments with a string/template-aware tokenizer; a `/*` or `//`
  inside a string or line comment no longer hides the imports after it.
- Read template-literal specifiers, import.meta.glob and `import(..., opts)`.
- Flag specifiers that leave src/v1 (`/`, `..`, `@/`, `src/`) itself, not
  only via Biome, which skips template literals.
- Check `@import`/`url()` and `ant-` in styles/ CSS; report symlinks;
  catch built `ant-` names; treat .test/.spec .mts/.cts as test files;
  ignore .DS_Store; drop the unused --root option.

biome.json: also ban `..`, `./../**`, `/**`, `antd/**`, `prop-types/**`.
tsconfig: skipLibCheck off so v1's own .d.ts files are checked.
.gitignore: re-include lib/ at any depth under src/v1.

Fixtures: a generated matrix over every folder-contract pair, every import
form, every outside spelling, a symlink case, and cases for each new rule;
tsc cases assert error codes; output matching excludes the command text and
spawn errors fail loudly. Comment and README corrections.
… (UN-4270)

- ci-frontend-lint: the typecheck job also runs `typecheck:v1` and
  `lint:v1`, so the strict v1 tsconfig and the scripts/check-v1.mjs checks
  (folder contract, TypeScript-only files, PropTypes, `ant-`) gate PRs now
  rather than from P0-T10.
- Pages may no longer import app/, test/ or the v1 entry, only the shared
  folders (components, shell, services, stores, hooks, lib, styles,
  plugins). Test files in a page may still import test/. Contract table,
  README and the fixture matrix updated together.
…(UN-4270)

check-v1.mjs: split the comment scanner into small helpers (cognitive
complexity 40 -> under 15), drop the nested ternary and nested template
literal, use String.raw for the import regexes, keep CLI flags in a Set.

Fixture runner:
- Cases and the expected folder contract are parsed text tables instead of
  repeated object literals (the 10% duplication).
- Sorts use an explicit localeCompare comparator.
- The contract matrix is built by a helper.
- Spawns /bin/sh by absolute path and no longer needs `git init` (Biome only
  needs the ignore file). The git check-ignore call keeps a PATH lookup, with
  the reason and a NOSONAR marker.

Fixture cases: re-exports rewritten as small functions and read-only props,
without changing what each case exercises.
…nnot see (UN-4270)

Addresses Greptile review on #2380:
- import.meta.glob([...]): every pattern in the array is checked, not only
  the first, so an allowed first pattern no longer hides a forbidden one.
- check-v1 --code also scans v0 (src/ outside v1, including the cloud
  src/plugins overlay) for template-literal imports, globs, require(),
  @/v1, src/v1, relative paths and CSS @import that reach into v1 other
  than through `@v1`. Biome still covers plain imports.

Fixtures: Greptile's example as a case, a glob array with a forbidden later
pattern, and a generated test of every v0 -> v1 form (denied and allowed).
The v0 scan reports nothing on the current tree.
…i 5.2 both declare containSubset

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… per-browser preference

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… match the v1 Tailwind root

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…heme mapping and the focus utility

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…so biome ci passes

src/v1/styles/tokens.css is written by scripts/build-v1-tokens.mjs and checked byte-for-byte by styles/tokens.test.ts; the formatter would rewrap it and break the check.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…sts and on missing variants

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…line styles, CSS imports and !important fail lint

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… and the fixtures

v0's root auto-detected sources across the whole frontend, so the v1 tree, scripts/check-v1-styles.mjs and test-fixtures/ added 12 unprefixed utilities and 8 theme variables to v0's stylesheet. With these three @source not lines the v0 build is byte-identical to the base again (239/239 files).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…longer cancel each other

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…our arbitrary values and !important after a variant

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…cope variants, shadows and the root fill to v1

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…the theme test exercises the mode effect

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ge, the pixel escape and the current unprefixed-class rationale

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Base automatically changed from UN-4299-design-tokens-themes to main November 4, 2026 04:35
@johnyrahul johnyrahul changed the title UN-4361 [DEV] Add v1 toasts, inline alerts, progress and skeletons behind one notification entry point UN-4361 [FEAT] Add v1 toasts, inline alerts, progress and skeletons behind one notification entry point Nov 4, 2026
…container changes

- Import Radix Progress as RadixProgress: main's shim-completeness test
  scans .tsx for <Foo.Bar> and read <Progress.Root> as the antd
  Progress shim, which has no Root.
- Toaster now re-targets its viewport when the container prop changes
  (Greptile finding).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HzRGGSAhDA4adWPAfcbhpt

@vishnuszipstack vishnuszipstack left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review of PR 2426 — UN-4361 v1 feedback components

Mode: INITIAL. Reviewed head c0dd48955812eb33f2d8b4dc162f2f3a7f72a8a7, merge base 332b4e3c0e5cb8237180f95f350d7c1aa7401bdb, base freshened (base_fetch: ok), 24 files, 1665 lines (matches GitHub). Rubric: unstract plugin v2.1.80.
Signals: lens 13 and component lifecycle raised (tests changed, React effects). facts.sh also raised the attack-surface and auth triggers; the only matching added lines I found use the word "token" for design tokens.
Security deep-dive: done
boundary-sweep: 1 sinks traced, clean (the notify() console fallback carries title, description and request id to the viewer's own browser console).

Verdict — REQUEST CHANGES

Summary — Critical: 0 · High: 1 · Medium: 8 · Low: 3 · Lenses run: 19/19 (1 not covered, 6 N/A; see checklist).

No v1 route renders these components and nothing calls notify() yet, so no user is affected today. Severities rate the contract that P1-T11, P1-T14 and P1-T18 will build on.

Findings are inline below, numbered as in the summary counts.

Unanchored findings

  • The PR body is out of date: "Five guardrail fixtures" (the diff adds seven rows) and "63 vitest tests and 94 guardrail fixture rows" (the run here gave 64 and 96). It also still says "Stacked on #2399. Retarget to main once it merges", and a PR comment repeats the merge-order warning, though the base is already main.
  • The CI report's ui row is red, but it is not this PR's: the same row fails on PRs 2430, 2424 and 2422, and the group is declared optional scaffolding in tests/groups.yaml. The e2e check passes.

Lens checklist

  1. Spec & intent — See findings #6
  2. Architectural fit & precedent — Clean
  3. Correctness & edge cases — See findings #1, #2, #3, #5, #6, #7, #8, #10
  4. Security — Clean
  5. Data integrity & migrations — N/A — no persisted data or schema
  6. Concurrency — Clean
  7. API & contract compatibility — See findings #1, #4, #11
  8. Reliability & resilience — N/A — no external calls
  9. Performance & cost — Clean
  10. Observability — See findings #4
  11. Operational safety — N/A — no route renders v1, and no infra or rollout config changed
  12. LLM/agent-specific — N/A — no model, prompt or agent path
  13. Testing — See findings #6, #9
  14. Dependencies & build — Clean
  15. Code quality — Clean
  16. Doc & comment accuracy — See findings #1, #2, #4, #6, #7, #8, #12
  17. Cross-document rule consistency — Not covered — merge-result sweep unavailable: git 2.34 here, the script needs 2.38
  18. On-prem deployment experience — N/A — no chart, values or on-prem surface touched
  19. Scaling & workload lifecycle — N/A — no worker or scaling config touched

Assessed by me rather than an agent: lenses 2 (with code-reviewer), 4, 5, 6, 8, 11, 12, 14, 15, 17, 18, 19.

  • Lens 4: the security deep-dive's find pass reported no candidates, so its filters had nothing to run on. It rests on that one pass plus my own read of the diff, and it excludes DoS and rate limiting by design.
  • Lens 15: biome ci src/v1, lint:v1 and typecheck:v1 all pass here.
  • Lens 17: branch-side grep of the changed rules found no disagreement beyond finding 12. The one commit origin/main gained since the merge base touches none of these paths.

Open questions for the author

  • Does aria-busy="true" on the role="status" skeleton (Skeleton.tsx:L29-L36) suppress the "Loading projects" label? The region is never set non-busy, it just unmounts, and role=status takes no name from content, though the test at feedback.test.tsx:L96-L113 is titled "one busy, named region". UNVERIFIED — an NVDA or VoiceOver pass would settle it.
  • Should a persistent toast outrank the three-toast cap? The frames or spec decide finding 2.
  • Should the request id be visible in the toast? That decides finding 4.

Assumptions made

  • Nothing in the design spec or frames was opened; the Confluence and Figma links were not read, so spec conformance in lens 1 is judged against the PR body and the code's own comments.
  • The console is treated as a safe sink because it is the viewer's own browser; I did not check whether a v1 shell will forward console output to a third party.
  • Probes ran under happy-dom. Layout, real pointer timing and screen-reader output were not exercised in a browser.

Dropped after checking: the "invisible click-blocking strip" from the cold read (Radix sets pointerEvents: none on the wrapper with no toasts, dist/index.mjs:L180), and the v0 svg.lucide rule collapsing icon sizes (it sits in @layer base, so ui:size-* utilities win; layer order only, not checked in a browser).

Comment thread frontend/src/v1/lib/notify.ts Resolved
Comment thread frontend/src/v1/lib/notify.ts Resolved
- Toaster: key and remove toasts by a per-show revision, so a replacement
  gets its own timer, persistence and announcement, and an action that
  re-notifies its own id is kept. At the cap, timed toasts drop before
  persistent ones. Focus moves to the viewport when a focused toast is
  removed. The provider remounts when the last toast goes, clearing
  Radix's stuck hover pause. The request id is shown and selectable.
  Swipe-to-dismiss is off (no swipe styles in the design). Actions and
  onRetry are called with no arguments.
- lint:v1: radix-ui is allowed outside components/ui/feedback/ only as a
  static named import that does not name Toast; every other reference is
  reported. Five new fixtures.
- ProgressBar: a non-finite value shows an empty bar.
- Skeleton: no aria-busy on the status region, so its label is read.
- notify/README: describe the no-notifier fallback and replacement
  accurately; lint:v1 row names the new rule.
- Tests for each behaviour, checked against mutants.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HzRGGSAhDA4adWPAfcbhpt

Copy link
Copy Markdown
Contributor Author

@vishnuszipstack thanks for the thorough review. All 12 findings are addressed in bb4ea8b, with replies on each thread. On the rest of your review:

  • PR body: updated. The counts are now 73 v1 vitest tests (728 across the frontend), 101 fixture rows and 12 notify fixtures. The stacked-on-UN-4299 [FEAT] v1 design tokens, themes and style isolation (P0-T04) #2399 note is gone, and I struck through the merge-order comment.
  • ui row in the CI report: agreed, it isn't this PR's (it's optional scaffolding, red on other PRs too), and e2e passes.
  • Skeleton aria-busy (open question): I dropped it. Per ARIA, aria-busy="true" on a live region tells assistive tech to hold announcements until it turns false, and the skeleton unmounts instead, so the "Loading projects" label could go unread. The region is now role="status", with the label as its only text. I haven't checked this with NVDA or VoiceOver; that would settle it.
  • Persistent vs the cap (open question): persistent wins over timed toasts. The cap still wins when all three are persistent (see finding 2).
  • Request ID visible (open question): yes, it's shown as a selectable caption line (finding 4).

Generated by Claude Code

…pties the list

Removing a focused toast while others stayed open left the refocus flag
set, so the next provider remount (when the last toast timed out) pulled
focus to the viewport, away from wherever the user had gone. Set the flag
only when the removal itself empties the list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01HzRGGSAhDA4adWPAfcbhpt
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Frontend Lint Report (Biome)

✅ All checks passed! No linting or formatting issues found.

@sonarqubecloud

sonarqubecloud Bot commented Nov 5, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
⚪ e2e-agent-kv e2e 0 0 0 1 0.1
✅ e2e-api-deployment e2e 3 0 0 0 17.0
✅ e2e-coowners e2e 1 0 0 0 1.3
✅ e2e-etl e2e 1 0 0 0 8.2
✅ e2e-login e2e 2 0 0 0 1.3
✅ e2e-prompt-studio e2e 1 0 0 0 12.5
✅ e2e-smoke e2e 2 0 0 0 2.5
✅ e2e-workflow e2e 1 0 0 0 18.3
❌ ui e2e 0 1 0 0 0.0
TOTAL 11 1 0 1 61.1

Critical paths

⚠️ Critical paths not yet covered

  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (declared coverage: no groups declared)
💤 Covered, but not exercised in this build
  • adapter-register-llm — Register and validate an LLM adapter. (covered by integration-backend; no result reported in this build)
  • workflow-author — Create a workflow; its source+destination endpoints materialise and are configurable. (covered by integration-backend; no result reported in this build)
  • api-deployment-provision — Deploying a workflow as an API mints a usable key and a resolvable endpoint. (covered by integration-backend; no result reported in this build)
  • api-deployment-auth — Unauthenticated or mis-scoped API-deployment calls are rejected before dispatch. (covered by integration-backend; no result reported in this build)
  • mcp-server-auth — Unauthenticated or mis-scoped hosted-MCP calls are rejected before any tool runs. (covered by integration-backend; no result reported in this build)
  • mcp-platform-auth — The org-scoped MCP endpoint stays behind the platform-API-key middleware; unauthenticated or mis-scoped calls reach no tool. (covered by integration-backend; no result reported in this build)
  • platform-key-whoami — A platform API key resolves its own organisation over the org-less whoami endpoint; the org comes from the key row, not the URL. (covered by integration-backend; no result reported in this build)
  • prompt-studio-author — Create a Prompt Studio project and add a prompt to it. (covered by integration-backend; no result reported in this build)
  • connector-register-test — Connector credentials are validated against the live system and stored encrypted. (covered by integration-backend; no result reported in this build)
  • usage-aggregate-read — Per-run token usage aggregates correctly and stays scoped to its organization. (covered by integration-backend; no result reported in this build)
✅ Covered critical paths
  • auth-login — covered by e2e-login
  • co-owner-manage — covered by e2e-coowners
  • workflow-create-execute — covered by e2e-workflow
  • api-deployment-run — covered by e2e-api-deployment
  • prompt-studio-fetch-response — covered by e2e-prompt-studio
  • pipeline-etl-execute — covered by e2e-etl
  • usage-token-tracking — covered by e2e-api-deployment
  • callback-result-delivery — covered by e2e-api-deployment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

main-only Stays on main: not backported to a release line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants