@clicktap/ui 0.42.4 → 0.43.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.
@@ -4,6 +4,10 @@ import { ResendCodeProps } from './ResendCode.types';
4
4
  * request and cooldown state (e.g. via a resend hook composing `useCooldown`). Renders one of three
5
5
  * labels: the cooldown countdown, the in-flight label, or the idle action label; the button is
6
6
  * disabled unless `canResend`.
7
+ *
8
+ * A visually-hidden polite live region announces when resend becomes available (CTAP-1167). The
9
+ * announcement is a separate region — not `aria-live` on the button — so screen-reader users hear
10
+ * the cooldown END without having every one-second countdown label change read out.
7
11
  */
8
- export declare function ResendCode({ cooldownSeconds, isResending, canResend, onResend, prompt, label, resendingLabel, className, buttonClassName, }: ResendCodeProps): import("react/jsx-runtime").JSX.Element;
12
+ export declare function ResendCode({ cooldownSeconds, isResending, canResend, onResend, prompt, label, resendingLabel, availableAnnouncement, className, buttonClassName, }: ResendCodeProps): import("react/jsx-runtime").JSX.Element;
9
13
  export default ResendCode;
@@ -1 +1 @@
1
- "use client";import{jsxs as u,jsx as b}from"react/jsx-runtime";import{cn as n}from"../../utils/cn.js";function f({cooldownSeconds:e,isResending:t,canResend:d,onResend:s,prompt:i="Didn't get a code?",label:l="Resend code",resendingLabel:o="Sending…",className:a,buttonClassName:r}){const c=e>0?`Resend in ${e}s`:t?o:l;return u("p",{className:n("text-sm",a),children:[i," ",b("button",{type:"button",onClick:s,disabled:!d,className:n("underline disabled:no-underline disabled:opacity-50 disabled:cursor-not-allowed",r),children:c})]})}export{f as ResendCode,f as default};
1
+ "use client";import{jsxs as m,jsx as t}from"react/jsx-runtime";import{cn as a}from"../../utils/cn.js";function x({cooldownSeconds:e,isResending:n,canResend:s,onResend:d,prompt:i="Didn't get a code?",label:l="Resend code",resendingLabel:r="Sending…",availableAnnouncement:o="You can request a new code now.",className:c,buttonClassName:u}){const b=e>0?`Resend in ${e}s`:n?r:l;return m("p",{className:a("text-sm",c),children:[i," ",t("button",{type:"button",onClick:d,disabled:!s,className:a("underline disabled:no-underline disabled:opacity-50 disabled:cursor-not-allowed",u),children:b}),t("span",{"aria-live":"polite",className:"sr-only",children:e===0&&!n?o:""})]})}export{x as ResendCode,x as default};
@@ -13,6 +13,11 @@ export type ResendCodeProps = {
13
13
  label?: string;
14
14
  /** Label shown while a resend is in flight. Defaults to "Sending…". */
15
15
  resendingLabel?: string;
16
+ /**
17
+ * Screen-reader announcement made (via a polite live region) when the cooldown ends and resend
18
+ * becomes available. Defaults to "You can request a new code now.".
19
+ */
20
+ availableAnnouncement?: string;
16
21
  /** className for the wrapping paragraph. */
17
22
  className?: string;
18
23
  /** className for the action button. */
@@ -1,6 +1,7 @@
1
1
  # `@clicktap/ui` resolves its deep subpaths under `moduleResolution: bundler` (CTAP-1060)
2
2
 
3
3
  **Date:** 2026-07-31
4
+ **Revised:** 2026-08-08 — the test-runner half (CTAP-1195): every runtime subpath entry gains a `default` condition, so CJS resolvers (Jest) stop falling through to the `.d.ts`. See "Test runners (Jest)" below; reported by noco.
4
5
  **Package:** `@clicktap/ui`
5
6
  **Breaking:** No — additive to the `exports` map. Existing `moduleResolution: node` / `node10`
6
7
  consumers are unaffected (that resolver ignores `exports`); this only *adds* working resolution under
@@ -43,8 +44,34 @@ emits only `.d.ts` for it).
43
44
  blocker — **nx 20.0.3 cannot build a project graph with TS 7 installed** (`Cannot read properties of
44
45
  undefined (reading 'readFile')`) — so TS 7 also needs an nx major.
45
46
 
47
+ ## Test runners (Jest) — the CJS half (CTAP-1195)
48
+
49
+ The original map declared only `types` + `import` conditions. That covers bundlers — but **Node CJS
50
+ resolution** (what Jest uses without ESM flags) matches neither, so it fell through to the `.d.ts`
51
+ and died on `export interface`. Any consumer whose Jest suite touches `useCooldown` (i.e. anyone
52
+ adopting the CTAP-1114 `useResendCode` pattern *with* its tests) hit this; noco worked around it with
53
+ a `moduleNameMapper` straight to the `.js` files.
54
+
55
+ Since this revision every runtime subpath entry carries a **`default` condition (last)** pointing at
56
+ the same ESM files as `import`, so CJS resolvers land on real JavaScript. Two consequences:
57
+
58
+ - **Drop any `moduleNameMapper` workaround** for `@clicktap/ui` paths after bumping — resolution now
59
+ works natively.
60
+ - **The transform allowance is still required**: the resolved files are ESM (`import`/`export`
61
+ syntax), which babel-jest must transpile. Keep `@clicktap/ui` out of `transformIgnorePatterns`,
62
+ e.g. under pnpm:
63
+
64
+ ```ts
65
+ transformIgnorePatterns: ['/node_modules/.pnpm/(?!(@clicktap\\+ui)@)'],
66
+ ```
67
+
68
+ A `default` condition cannot remove that half — only a real CJS build could, which is out of
69
+ proportion for an ESM-only `'use client'` package.
70
+
46
71
  ## Verified
47
72
 
48
73
  Resolution was tested against the built package in both modes: under `bundler` all deep subpaths (flat
49
74
  file, directory-index, and nested file) resolve **with types**; under `node` (node10) resolution is
50
- unchanged (no regression).
75
+ unchanged (no regression). For the CTAP-1195 revision: `require.resolve('@clicktap/ui/hooks/useCooldown')`
76
+ from a CJS context resolves to the `.js` (previously an `ERR_PACKAGE_PATH_NOT_EXPORTED`-adjacent fall-through
77
+ to types).
@@ -16,9 +16,16 @@ export interface UseCooldownResult {
16
16
  /**
17
17
  * A headless one-second countdown. `start(n)` sets the remaining seconds and it ticks down to zero.
18
18
  *
19
- * StrictMode-safe: the tick lives in an effect (not a state updater), and the countdown is mirrored
20
- * into a ref so the effect reads the latest value without re-subscribing each second and so callers
21
- * get a stale-free view via `isActiveNow()`.
19
+ * Deadline-based (CTAP-1167): `start(n)` stores an absolute deadline and every tick recomputes the
20
+ * remaining time from the clock instead of decrementing. Browsers and WebViews throttle timers in
21
+ * backgrounded tabs (Capacitor apps aggressively), so a decrementing counter falls behind real
22
+ * time — a 60s cooldown resumed after backgrounding would still show tens of seconds long after
23
+ * the real deadline passed. Recomputing makes the first tick after a throttled gap snap to the
24
+ * true remaining time, and `isActiveNow()` compares against the deadline itself so re-entry
25
+ * guards are immune to a stale count.
26
+ *
27
+ * StrictMode-safe: the tick lives in an effect (not a state updater), and the deadline is a ref so
28
+ * the timer chain reads the latest value without re-subscribing each second.
22
29
  */
23
30
  export declare function useCooldown(): UseCooldownResult;
24
31
  export default useCooldown;
@@ -1 +1 @@
1
- "use client";import{useState as i,useRef as f,useCallback as r,useEffect as l}from"react";function d(){const[s,n]=i(0),t=f(0),o=r(c=>{const e=Math.max(0,Math.ceil(c));t.current=e,n(e)},[]),u=r(()=>{t.current=0,n(0)},[]),a=r(()=>t.current>0,[]);return l(()=>{if(s<=0)return;const c=setTimeout(()=>{const e=Math.max(0,t.current-1);t.current=e,n(e)},1e3);return()=>clearTimeout(c)},[s]),{seconds:s,isActive:s>0,start:o,reset:u,isActiveNow:a}}export{d as default,d as useCooldown};
1
+ "use client";import{useState as f,useRef as d,useCallback as r,useEffect as w}from"react";function M(){const[u,o]=f(0),e=d(0),i=r(n=>{const t=Math.max(0,Math.ceil(n));e.current=t>0?Date.now()+t*1e3:0,o(t)},[]),l=r(()=>{e.current=0,o(0)},[]),m=r(()=>e.current>Date.now(),[]),c=u>0;return w(()=>{if(!c)return;let n;const t=()=>{const s=e.current-Date.now();return s>0?s%1e3||1e3:0},a=()=>{const s=Math.max(0,Math.ceil((e.current-Date.now())/1e3));o(s),s>0&&(n=setTimeout(a,t()))};return n=setTimeout(a,t()),()=>clearTimeout(n)},[c]),{seconds:u,isActive:c,start:i,reset:l,isActiveNow:m}}export{M as default,M as useCooldown};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clicktap/ui",
3
- "version": "0.42.4",
3
+ "version": "0.43.1",
4
4
  "private": false,
5
5
  "author": "Clicktap",
6
6
  "description": "A library of React UI components and low-level hooks.",
@@ -25,6 +25,10 @@
25
25
  "import": [
26
26
  "./components/*.js",
27
27
  "./components/*/index.js"
28
+ ],
29
+ "default": [
30
+ "./components/*.js",
31
+ "./components/*/index.js"
28
32
  ]
29
33
  },
30
34
  "./next": {
@@ -33,6 +37,9 @@
33
37
  ],
34
38
  "import": [
35
39
  "./next/index.js"
40
+ ],
41
+ "default": [
42
+ "./next/index.js"
36
43
  ]
37
44
  },
38
45
  "./next/*": {
@@ -43,6 +50,10 @@
43
50
  "import": [
44
51
  "./next/*.js",
45
52
  "./next/*/index.js"
53
+ ],
54
+ "default": [
55
+ "./next/*.js",
56
+ "./next/*/index.js"
46
57
  ]
47
58
  },
48
59
  "./hooks/*": {
@@ -53,6 +64,10 @@
53
64
  "import": [
54
65
  "./hooks/*.js",
55
66
  "./hooks/*/index.js"
67
+ ],
68
+ "default": [
69
+ "./hooks/*.js",
70
+ "./hooks/*/index.js"
56
71
  ]
57
72
  },
58
73
  "./utils/*": {
@@ -63,6 +78,10 @@
63
78
  "import": [
64
79
  "./utils/*.js",
65
80
  "./utils/*/index.js"
81
+ ],
82
+ "default": [
83
+ "./utils/*.js",
84
+ "./utils/*/index.js"
66
85
  ]
67
86
  },
68
87
  "./types/*": {