Skip to content

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

Open
johnyrahul wants to merge 41 commits into
mainfrom
UN-4361-feedback
Open

johnyrahul wants to merge 41 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; optional action; persistent; stacks bottom-right (at most three, oldest drops first); pauses while hovered or focused; F8 moves focus to it; replacing by id.
    • InlineAlert: dev spec section 8 error state, "inline, above the content it concerns, with a retry".
    • ProgressBar (Radix Progress, determinate and indeterminate, name required) 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 it logs with the request id, so a failure is never silent.
  • lint:v1: check-v1.mjs rejects sonner, react-hot-toast, react-toastify, notistack and Radix Toast outside components/ui/feedback/ (static, dynamic, named and namespace imports). Five guardrail fixtures.
  • README: a Feedback section.

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 must 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.
Errors announce assertively (type="foreground"), everything else politely Spec: "error toasts are assertive".
Markup snapshot is __snapshots__/markup.md via toMatchFileSnapshot typecheck:v1 rejects .snap files under v1 (not in the allowed types). Heads-up: #2416 commits a .snap and will hit this on #2399's latest head.
Tests use plain matchers src/v1/tsconfig.json has no jest-dom types; adding them is P0-T08's harness.
ui:box-border on padded full-width boxes v1 ships no preflight, so w-full plus padding overflowed (seen in the browser: toasts ran off the screen edge).
Stand-ins for missing tokens No info token (uses primary); no destructive-muted (InlineAlert fill is muted, as in frame 622:1320); skeleton radius 4 and bar colour #e4e4e7 are not tokens (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, biome ci src/ clean; bun run test:v1: 63 vitest tests and 94 guardrail fixture rows pass; bun run build succeeds.
  • Toaster tests cover timeout, persistent, pause on hover, action, close/Escape/by-id dismissal, F8 keyboard reach, the three-toast cap, replace by id, assertive vs polite announcement, unmount fallback and the portal container.
  • Rendered in Chromium in Light and Dark through a throwaway Vite page (screenshots below).

Screenshots

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 <noreply@anthropic.com>
… per-browser preference

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in three modes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… match the v1 Tailwind root

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ercase letters only

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…heme mapping and the focus utility

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sts and on missing variants

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and portal sync

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…line styles, CSS imports and !important fail lint

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 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 <noreply@anthropic.com>
…longer cancel each other

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…our arbitrary values and !important after a variant

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cope variants, shadows and the root fill to v1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the theme test exercises the mode effect

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ge, the pixel escape and the current unprefixed-class rationale

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
praveen-formido and others added 10 commits October 29, 2026 13:48
Runtime geometry (a page's aspect ratio, a highlight's position) cannot be
a token or a class. Rule S1 now accepts a style attribute whose value is an
object literal with only "--name" keys, so the numbers travel through custom
properties that ui: classes read (ui:aspect-(--page-aspect)); a string, a
variable, a spread or a plain key still fails. Three fixtures pin it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pre-commit's check-shebang-scripts-are-executable fails on branches stacked
on this one; check-v1.mjs already carries the bit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
React's CSSProperties has no index signature for "--*" keys, so the object
literal rule S1 allows fails TS2353 under strict mode. Augment it under
lib/, and typecheck the passing fixture so the augmentation is exercised.
Raised by Ali on the viewer stack.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
check-v1-styles.mjs let banned styling through in six ways, each
reproduced on this branch before the fix:

- S8 accepted `hover:ui:bg-primary` and `md:ui:flex`; Tailwind 4 needs
  the prefix first and compiles those to nothing. A prefix after a
  variant is now reported with its own message.
- S2 missed named colours as whole JSX values (`fill="red"`) and in CSS
  declarations (`color: white`); both now reuse the named-colour list,
  with transparent and currentcolor still allowed.
- S3 matched a unit only right before `]`, so `grid-cols-[200px_1fr]`
  and `w-[calc(100%-13px)]` passed; it now matches anywhere inside the
  brackets. The CSS pattern gains vh and vw, which the TSX side already
  rejected.
- S8 skipped a single-word class (`className="flex"`) because the
  class-string heuristic needed two utilities; the value of className
  and the arguments of cn()/cva()/clsx()/cx() are now always checked.
- S6 accepted `.v1 + div` and `.v1 ~ div`, and never looked at nested
  rules, so `& + div` escaped the root too. A sibling combinator right
  after the root now fails, at top level and nested.
- S1 missed `style = {{ … }}` with spaces around the `=`.

Nine fixtures pin the gaps; the passing fixture gains the forms the
tightened rules must still allow (prefix-first variants, `.v1 > a + b`,
nested `& > a`, a non-utility single word, transparent in CSS).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e tokens, share the store's storage

- Each generated `.v1[data-theme=…]` block now starts with the mode's
  `color-scheme` (dark for dark, light otherwise), declared beside the
  modes in lib/theme.ts. Without it native controls, select popups,
  autofill and scrollbars kept the browser's light palette on a dark
  v1 page, and S7 rules out the prefers-color-scheme fallback.
- The spacing utilities read the spacing tokens: `--spacing` is bound to
  `--space-1` so every bare step (`ui:w-64`, `ui:size-4`) multiplies the
  token, and the named steps 1 to 8 read their own token, so `ui:p-5`
  is `var(--space-5)` as the token table says. The compile test asserts
  both forms.
- `hydrate()` defaults to the storage the store writes through, and the
  provider's duplicate localStorage lookup is gone, so the preference
  is read from where it was written.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`tokens:v1` used node with a `registerHooks` resolver so the generator
could import the TypeScript sources; that API needs Node 22.15 while
the engines field allows 20.19, so the script failed on the oldest
supported Node. bun, which the frontend toolchain already uses, imports
the sources and resolves extensionless imports directly, so the hook is
removed. The generated tokens.css is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…fold has landed

The scaffold PR #2380 was squash-merged, so every scaffold file both sides
touched conflicted. Main's version is taken for the import checker, the
tsconfigs, the CI workflow and the Biome boundary groups; this branch keeps
the styling policy script and its lint:v1 and tokens:v1 entries, the
tokens.css formatter override, the Styling section of the v1 README and the
27 styling fixture rows. Two fixtures main re-added or changed carried an
unscoped :root selector with a raw hex, which the styling policy rejects, so
they reference a token under .v1 instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… from review

S3 let a unit followed by `_` through (`ui:grid-cols-[200px_1fr]`,
`ui:m-[2rem_auto]`): the lookahead after the unit rejected `\w`, which
includes Tailwind's in-bracket separator. It now rejects letters and `-`
only, and fixture v1-raw-pixel-arbitrary-separator pins both forms.

S8 read every literal inside cva() as a class list, so a normal
`defaultVariants: { variant: "outline" }` failed because outline is also a
utility name. classSiteLiterals now tracks the property chain inside cva()
and treats as classes only the base string, the values under variants and
a compound `class`/`className`; a property name, a defaultVariants value or
a compoundVariants matcher is a choice, not a class. The passing fixture
carries a full cva config; v1-cva-unprefixed-variant proves a variant
value is still checked.

S6 reported every nested `& + x` as leaving the root, but under `.v1 .menu`
it resolves to `.v1 .menu + x`, inside v1. selectorPreludes now records
whether every enclosing rule is one compound on the root, and only then
does a `&` sibling leave. The nested fixture is rewritten to the leaving
forms (directly under `.v1`, and under `&[data-theme="dark"]`), and the
passing fixture gains the safe ones.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…narCloud findings

SonarCloud's duplication check ignores literal values, so the 75 same-shaped
entries in styles/tokens.ts read as copies of each other (91% of the file,
16.8% of the PR's new lines, gate threshold 3%). The values now live in
styles/tokens.json, one array per family in board order; tokens.ts keeps the
Token type and builds TOKENS from it, deriving cssVar from name (true for
every entry) and expanding a single string to all three modes. tokens.css is
regenerated (header only) and the lint no longer needs a raw-values exemption
for tokens.ts.

Also, from Ritwik's pass over the remaining findings:
- CSS_UNIT_RE: `\d+(?:\.\d*)?|\.\d+` matches the same strings without the
  overlapping alternation (javascript:S8786).
- The stem escape `s.replace("-", "\\-")` was a no-op: `-` is literal outside
  a character class.
- The file-type dispatch uses `endsWith(".css")` (javascript:S6557).
- `.sonarcloud.properties` excludes frontend/test-fixtures/**: the boundary
  fixtures break the v1 rules on purpose.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…on entry point

Toasts (Radix Toast), InlineAlert with retry, ProgressBar, StepProgress,
Skeleton primitives and Spinner under components/ui/feedback/, plus the
notify() contract in lib/notify.ts that the Toaster implements and P0-T05's
error normaliser can call. lint:v1 rejects toast libraries outside the
feedback folder, with guardrail fixtures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HzRGGSAhDA4adWPAfcbhpt
@johnyrahul johnyrahul self-assigned this Nov 3, 2026
@greptile-apps

greptile-apps Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

via Greptile

RetriggerView in GreptileConfidence Score: 5/5

[Low impact] The PR appears safe to merge, with a non-blocking gap in the toast replacement test.

Summary

Adds v1 feedback components behind notify() and toast.*.

  • V1 callers send notifications through one shared entry point.
  • Inline alerts show errors and offer an optional retry action.
  • Progress indicators show task completion and the current step.
  • Skeletons and small spinners show when v1 content is loading.

Reviews (2) · Last reviewed commit: "UN-4361 [FIX] Close the Greptile finding..." · Reviewed by Greptile

Comment thread frontend/scripts/check-v1.mjs Resolved
- The single-entry-point lint now also catches `await import("radix-ui")`
  (namespace or destructured Toast) and `Radix["Toast"]`, with fixtures.
- ProgressBar: an invalid or zero max shows an empty bar instead of NaN,
  and the indeterminate pulse respects reduced motion.
- The replace-by-id test replaces an already-open toast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HzRGGSAhDA4adWPAfcbhpt
@sonarqubecloud

sonarqubecloud Bot commented Nov 3, 2026

Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor Author

Merge order: this PR is stacked on #2399 (design tokens and themes), so please merge it only after #2399 is merged. Once #2399 is in, this PR will be retargeted to main.


Generated by Claude Code

Base automatically changed from UN-4299-design-tokens-themes to main November 4, 2026 04:35
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.

3 participants