Skip to content

UN-4287 [FEAT] Add the v1 text inputs on one field frame - #2416

Draft
athul-rs wants to merge 40 commits into
mainfrom
UN-4287-v1-text-inputs
Draft

athul-rs wants to merge 40 commits into
mainfrom
UN-4287-v1-text-inputs

Conversation

@athul-rs

Copy link
Copy Markdown
Contributor

What

  • Frontend v1 text inputs (P1-T03), in frontend/src/v1/components/ui/form/, all drawn by one FieldFrame (border, radius, padding, focus ring and the hover, invalid, disabled and read-only states):
    • Input: height 32, with leading and trailing icons and prefix and suffix text.
    • Textarea, and PromptTextarea: at least 104 tall, grows with its text to a maximum, then scrolls. Enter submits, Shift+Enter breaks the line, and an IME composition never submits.
    • SearchField: Escape clears it when it has a value; on an empty field Escape reaches the dialog or popover around it. A Clear button does the same for the mouse.
    • SecretField: masked by default; Reveal (aria-pressed) and Copy, which copies the real value and reports the result through onCopyResult.
    • Field, Label, HelperText, ErrorText: Field wires the label, aria-describedby and aria-invalid for whichever control it holds; ErrorText is announced (role="alert").
  • lib/compose-refs.ts, which merges a component's own ref with the caller's, including React 19 ref cleanups.

Why

  • P1-T03 (UN-4287): the shared text controls every v1 form uses (P1-T06 form system, settings, prompt cards).

How

  • Built on the design tokens PR UN-4299 [FEAT] v1 design tokens, themes and style isolation (P0-T04) #2399: ui: classes only, no raw colours or lengths. Styling follows the kit's Input Group frames (216:8125) and the search and prompt spec frames (676:9483, 676:9513, 676:9543).
  • SearchField inside a Radix dialog: Radix calls preventDefault on Escape in the capture phase when the dialog chooses not to close. The field ignores that, so a dialog that skips dismissal for targets carrying data-escape-clears gets "Escape clears the field, then closes the dialog", which a test with a real Radix Dialog checks. Only a preventDefault from the caller's own onKeyDown cancels the clear.
  • SecretField sets autoComplete="new-password", data-1p-ignore, data-lpignore and turns off spellcheck, autocapitalise and autocorrect, so password managers and browser spellcheck services leave the value alone; callers cannot override these. Copy on an empty field writes nothing and reports {status: "empty"}.
  • onCopyResult is required: no toast component exists yet (P1-T13 builds it), so the caller raises the toast. This is the seam to swap when it lands.
  • Where field-sizing: content is unsupported (Firefox), PromptTextarea sizes itself from scrollHeight through the --prompt-height custom property, and re-fits after a form reset.

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 is new under frontend/src/v1/, which no v0 route renders. No v0 file, dependency or build file changes.

Database Migrations

  • None.

Env Config

  • None.

Relevant Docs

  • Task spec: P1-T03 on the Frontend v1 Confluence space. Designs repo pinned at 83e4fff.

Related Issues or PRs

Dependencies Versions

  • None.

Notes on Testing

  • lint:v1, typecheck:v1 and biome check src/v1 clean. test:v1: 118 vitest tests (83 on the base) and 59 boundary fixtures. The same checks pass on a merge with the newest scaffold commits (65 fixtures).
  • Every behavioural guard was mutation-checked (Shift+Enter, IME composition including Safari's keyCode 229, Escape clear and bubbling, the Radix dialog case, read-only and disabled locks from props and from Field, copy result handling, empty copy, privacy attributes, ref cleanups, the fallback re-fit): removing each one fails a test.
  • Reviewed locally with the team's 19-lens review (no Critical or High; the Mediums are fixed in this branch).

Design questions (no frame answers them)

  1. Input value text and radius: the dev-spec board says 13/18 and radius 8, the kit and search frame 14/20 and radius 10, the prompt frame 13/20 and radius 8. This PR uses 14/20, radius 8.
  2. Dark-mode field fill: the kit names dark:input/30, which no token carries. This PR uses --background in every theme.
  3. Invalid focus ring: --destructive at 50% (one focus treatment) or the kit's raw red at 20%? Disabled fill #e5e5e540 has no token.
  4. Hover and read-only states for inputs, the helper and error text under a control, a required marker, SecretField's Reveal and Copy, and SearchField's Clear have no frame.
  5. PromptTextarea's maximum height has no frame; 240 is a placeholder.

Screenshots

  • None yet; no v1 route renders these components.

Checklist

I have read and understood the Contribution Guidelines.

🤖 Generated with 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]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
A component that needs its own node and also forwards the caller's ref
feeds both through one callback ref.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
FieldFrame gives every text control the same border, radius, padding
and focus ring, with hover, invalid, disabled and read-only states as
data attributes. On it: Input (32 tall, leading/trailing icons and
prefix/suffix text), Textarea, PromptTextarea (104 minimum, grows to a
maximum then scrolls; Enter submits, Shift+Enter breaks the line, an
IME composition never submits), SearchField (Escape clears a value and
stops; on an empty field it bubbles) and SecretField (masked, Reveal
with aria-pressed, Copy writes the real value and reports the outcome
to the caller for the toast).

Field wires Label, HelperText and ErrorText to the control: the label
names it, aria-describedby lists the rendered texts, and an error sets
aria-invalid and renders as role=alert.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A caller that passes id now gets stable describedby ids, and the markup
snapshot no longer depends on useId render order.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…up frames

Fill is the background token (216:8126), inputs pad 10 with a 6 gap
(676:9487), the prompt pads 10 vertically (676:9547), and a label sits 6
above its control (639:5296).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The composed callback now returns a cleanup, so React calls it instead
of the callback with null: a ref that returned its own cleanup gets it
run, and every other ref is set to null.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
SearchField: a Radix dialog that skips dismissal for data-escape-clears
calls preventDefault in the capture phase, so the clear now ignores a
preventDefault made before it; only the caller's own onKeyDown cancels
it. The lock reads disabled and read-only from props and Field alike,
and a controlled field with no onValueChange lets Escape through.

PromptTextarea: Enter does not submit when read-only. Where
field-sizing is missing, it re-fits after its form resets and keeps the
height automatic while scrollHeight is 0.

SecretField: onCopyResult is required and is called once, after the
clipboard write, so a throwing handler is not reported as a failed
copy. An empty value is not written and reports status empty.
Reveal and Copy follow a disabled Field and stay usable when
read-only. new-password, the password-manager ignore hints and
spellcheck off are set after the props spread.

Tests cover each fix, a real Radix Dialog, and the wiring of all five
controls to Field.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Object and callback refs reach the control, and are released on
unmount, for the three controls that compose refs. FieldAction inside a
form clicks without submitting it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@sonarqubecloud

Copy link
Copy Markdown

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