@clicktap/ui 0.41.0 → 0.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- "use client";import{jsxs as s,jsx as e}from"react/jsx-runtime";import{Label as u,TextField as v,Input as k,Text as g,FieldError as L}from"react-aria-components";import{forwardRef as C,useState as I}from"react";import{cn as t}from"../../utils/cn.js";import{Skeleton as N}from"../Skeleton/Skeleton.js";import S from"../../hooks/useIsClient.js";function j({className:r}){return s("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor",className:r,children:[e("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.64 0 8.577 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.64 0-8.577-3.007-9.963-7.178Z"}),e("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"})]})}function y({className:r}){return e("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor",className:r,children:e("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88"})})}function M({className:r}){return e(N,{className:t("w-full h-10 rounded-md z-20 relative",r)})}const W=C(({inputProps:r,label:n,description:i,errorMessage:x,placeholder:f,className:d,classNames:o,disableSkeleton:p,type:c,...m},h)=>{const w=S(),a=c==="password",[l,b]=I(!1);return!w&&!p?s("div",{className:t("flex flex-col w-full text-slate-900",d),children:[n?e(u,{className:t("flex text-slate-500 text-xs",o?.label),children:n}):null,e(M,{className:o?.skeleton})]}):s(v,{className:t("flex flex-col w-full text-slate-900",d),type:a?l?"text":"password":c,...m,children:[e(u,{className:t("flex text-slate-500 text-xs",o?.label),children:n}),s("div",{className:"relative",children:[e(k,{ref:h,placeholder:f,className:t("border-solid border border-slate-300 rounded-md","text-sm text-slate-900 placeholder-slate-400","h-10 px-2 py-0 m-0 w-full","bg-white","transition-all duration-200 ease-in-out","hover:border-slate-400","focus:outline-2 focus:outline focus:outline-slate-200 focus:border-slate-400","disabled:border-slate-200 disabled:bg-slate-100","invalid:border-red-500 invalid:bg-red-100 invalid:text-red-600","invalid:hover:border-red-600 invalid:focus:border-red-600 invalid:focus:outline-red-200",a&&"pr-10",o?.input),...r}),a&&e("button",{type:"button",className:"absolute right-2 top-1/2 -translate-y-1/2 p-1 text-slate-400 hover:text-slate-600 bg-transparent border-none cursor-pointer",onClick:()=>b(!l),"aria-label":l?"Hide password":"Show password",tabIndex:-1,children:l?e(y,{className:"w-5 h-5"}):e(j,{className:"w-5 h-5"})})]}),i&&e(g,{className:t("flex text-slate-500 text-sm",o?.description),slot:"description",children:i}),e(L,{className:t("flex text-red-500 text-sm",o?.error),children:x})]})});export{W as Input,W as default};
1
+ "use client";import{jsxs as l,jsx as e}from"react/jsx-runtime";import{Label as x,TextField as k,Input as g,Text as L,FieldError as C}from"react-aria-components";import{forwardRef as I,useState as N}from"react";import{cn as t}from"../../utils/cn.js";import{Skeleton as P}from"../Skeleton/Skeleton.js";import M from"../../hooks/useIsClient.js";function S({className:r}){return l("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor",className:r,children:[e("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.64 0 8.577 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.64 0-8.577-3.007-9.963-7.178Z"}),e("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"})]})}function j({className:r}){return e("svg",{xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24",strokeWidth:1.5,stroke:"currentColor",className:r,children:e("path",{strokeLinecap:"round",strokeLinejoin:"round",d:"M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.451 10.451 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.522 10.522 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88"})})}function y({className:r}){return e(P,{className:t("w-full h-10 rounded-md z-20 relative",r)})}const W=I(({inputProps:r,label:s,description:d,errorMessage:p,placeholder:h,className:u,classNames:o,disableSkeleton:m,type:c,...w},b)=>{const v=M(),a=c==="password",[f,n]=N(!1);return!v&&!m?l("div",{className:t("flex flex-col w-full text-slate-900",u),children:[s?e(x,{className:t("flex text-slate-500 text-xs",o?.label),children:s}):null,e(y,{className:o?.skeleton})]}):l(k,{className:t("flex flex-col w-full text-slate-900",u),type:a?f?"text":"password":c,...w,children:[e(x,{className:t("flex text-slate-500 text-xs",o?.label),children:s}),l("div",{className:"relative",children:[e(g,{ref:b,placeholder:h,className:t("border-solid border border-slate-300 rounded-md","text-sm text-slate-900 placeholder-slate-400","h-10 px-2 py-0 m-0 w-full","bg-white","transition-all duration-200 ease-in-out","hover:border-slate-400","focus:outline-2 focus:outline focus:outline-slate-200 focus:border-slate-400","disabled:border-slate-200 disabled:bg-slate-100","invalid:border-red-500 invalid:bg-red-100 invalid:text-red-600","invalid:hover:border-red-600 invalid:focus:border-red-600 invalid:focus:outline-red-200",a&&"pr-10",o?.input),...r}),a&&e("button",{type:"button",className:"absolute right-2 top-1/2 -translate-y-1/2 p-1 text-slate-400 hover:text-slate-600 bg-transparent border-none cursor-pointer select-none",onPointerDown:i=>{i.preventDefault(),n(!0)},onPointerUp:()=>n(!1),onPointerLeave:()=>n(!1),onPointerCancel:()=>n(!1),onContextMenu:i=>i.preventDefault(),"aria-label":"Show password while pressed",tabIndex:-1,children:f?e(j,{className:"w-5 h-5"}):e(S,{className:"w-5 h-5"})})]}),d&&e(L,{className:t("flex text-slate-500 text-sm",o?.description),slot:"description",children:d}),e(C,{className:t("flex text-red-500 text-sm",o?.error),children:p})]})});export{W as Input,W as default};
@@ -0,0 +1,48 @@
1
+ # Password reveal is press-and-hold, not a toggle
2
+
3
+ **Date:** 2026-08-04
4
+ **Affects:** `@clicktap/ui` — `Input` with `type="password"`.
5
+ **Classification:** IMPROVEMENT (security / UX)
6
+ **Breaking:** Behavioural only — the reveal changed from a click-toggle to press-and-hold. No API/prop
7
+ change; adoption is automatic.
8
+ **Ticket:** CTAP-1128
9
+
10
+ ## Summary
11
+
12
+ The password-field reveal (the eye button in `Input`) now **shows the value only while the control is
13
+ actively held**, and hides it the instant the press ends. Previously it was a click **toggle**, which
14
+ could leave a password on screen after a stray click and widened the shoulder-surfing window. Press-and-
15
+ hold ties visibility to the moment of intent.
16
+
17
+ ## What changed
18
+
19
+ The reveal button swapped its `onClick` toggle for pointer press-and-hold:
20
+
21
+ ```tsx
22
+ onPointerDown={(e) => { e.preventDefault(); setShowPassword(true); }}
23
+ onPointerUp={() => setShowPassword(false)}
24
+ onPointerLeave={() => setShowPassword(false)}
25
+ onPointerCancel={() => setShowPassword(false)}
26
+ onContextMenu={(e) => e.preventDefault()}
27
+ aria-label="Show password while pressed"
28
+ tabIndex={-1}
29
+ ```
30
+
31
+ - **Pointer events** (not mouse-only) so mouse and touch both work from one code path.
32
+ - `preventDefault()` on press keeps focus on the input while peeking (you can keep typing).
33
+ - `onPointerLeave` / `onPointerCancel` cover dragging off the button or an interrupted touch, so the
34
+ value can't get stuck visible.
35
+ - **Unchanged:** the reveal is still pointer-only (`tabIndex={-1}`, not in the tab order) — this migration
36
+ does not add keyboard reveal.
37
+
38
+ ## Action required (consumer)
39
+
40
+ - **Using `@clicktap/ui` `Input`:** nothing — the behaviour ships with the component.
41
+ - **Rolled your own password reveal:** mirror the pattern above (swap the `onClick` toggle for the
42
+ pointer handlers) so behaviour is consistent across your app. The reference frontend also updated its
43
+ one bespoke reveal (`UpdateEmailForm`) to match.
44
+
45
+ ## Verify
46
+
47
+ Press and hold the eye control → the characters show; release (or drag the pointer off) → they hide
48
+ again. A single click no longer leaves the password visible.
@@ -28,5 +28,6 @@ For migrations affecting the frontend app, see [frontend migrations](../../../..
28
28
  | 2026-06-17 | [Select standardization — one `<Select searchable>`, `DropdownSelect` removed](./2026/06/2026-06-17-select-standardization.md) ↔ | Yes | *None* |
29
29
  | 2026-06-18 | [Select (searchable): fix "setValue is not a function" crash on browser autofill](./2026/06/2026-06-18-select-hiddenselect-setvalue-crash.md) | No | Select standardization |
30
30
  | 2026-08-04 | [`ResendCode` control + `useCooldown` hook](./2026/08/2026-08-04-resend-code-control.md) ↔ | No | OTP resend timer (CTAP-1114) |
31
+ | 2026-08-04 | [Password reveal is press-and-hold, not a toggle](./2026/08/2026-08-04-password-reveal-press-and-hold.md) | Behavioural | *None* (CTAP-1128) |
31
32
 
32
33
  ↔ = cross-app migration (has counterpart in frontend app)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clicktap/ui",
3
- "version": "0.41.0",
3
+ "version": "0.41.1",
4
4
  "private": false,
5
5
  "author": "Clicktap",
6
6
  "description": "A library of React UI components and low-level hooks.",