@clicktap/ui 2.4.3 → 2.4.5

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,19 +1,4 @@
1
1
  import { ComponentType, ReactNode } from 'react';
2
- /**
3
- * Native accordion on react-aria-components Disclosure (CTAP-1383).
4
- *
5
- * Previously a styled wrapper around @heroui/accordion — the ONLY
6
- * initially-rendered @heroui consumer (the PLP filter sidebar), which pulled
7
- * the whole @heroui system + theme (~25 KiB gz) into every page's initial JS.
8
- * react-aria-components is already this library's base, so the rewrite costs
9
- * nothing new. API-compatible with the call sites this repo uses:
10
- * selectionMode / defaultSelectedKeys ("all" included), per-item title,
11
- * subtitle, startContent, indicator (component, receives nothing, rotates on
12
- * open), classNames slots, showDivider (an <hr> between items — consumers
13
- * hide it with [&_hr]:hidden), onPress, and the `data-open` attribute on
14
- * heading/trigger/indicator that consumers style with data-[open=true]:.
15
- * Height animates with the CSS grid-rows trick — no animation runtime.
16
- */
17
2
  type ItemSlotClassNames = {
18
3
  base?: string;
19
4
  heading?: string;
@@ -38,6 +23,8 @@ export type AccordionItemProps = {
38
23
  hideIndicator?: boolean;
39
24
  disableIndicatorAnimation?: boolean;
40
25
  isDisabled?: boolean;
26
+ /** Overrides the Accordion's keepContentMounted for this item. */
27
+ keepContentMounted?: boolean;
41
28
  textValue?: string;
42
29
  'aria-label'?: string;
43
30
  classNames?: ItemSlotClassNames;
@@ -53,6 +40,10 @@ export type AccordionProps = {
53
40
  selectedKeys?: 'all' | Iterable<string>;
54
41
  onSelectionChange?: (keys: Set<string>) => void;
55
42
  disallowEmptySelection?: boolean;
43
+ /**
44
+ * Keep every collapsed item's content mounted (and server-rendered). Off by
45
+ * default: a collapsed item renders its content only while expanded.
46
+ */
56
47
  keepContentMounted?: boolean;
57
48
  /** Hide every item's indicator (heroui group-level prop). */
58
49
  hideIndicator?: boolean;
@@ -73,6 +64,6 @@ export type AccordionProps = {
73
64
  * did not change.
74
65
  */
75
66
  export declare function AccordionItem(_props: AccordionItemProps): null;
76
- export declare function Accordion({ children, selectionMode, defaultSelectedKeys, defaultExpandedKeys, selectedKeys, onSelectionChange, disallowEmptySelection, hideIndicator: groupHideIndicator, showDivider, dividerProps, variant, isCompact, className, itemClasses, }: AccordionProps): import("react/jsx-runtime").JSX.Element;
67
+ export declare function Accordion({ children, selectionMode, defaultSelectedKeys, defaultExpandedKeys, selectedKeys, onSelectionChange, disallowEmptySelection, keepContentMounted: groupKeepContentMounted, hideIndicator: groupHideIndicator, showDivider, dividerProps, variant, isCompact, className, itemClasses, }: AccordionProps): import("react/jsx-runtime").JSX.Element;
77
68
  export default Accordion;
78
69
  export declare function AccordionItemArrow(): import("react/jsx-runtime").JSX.Element;
@@ -1 +1 @@
1
- "use client";import{jsx as e,jsxs as d,Fragment as O}from"react/jsx-runtime";import{Children as V,isValidElement as _,Fragment as q}from"react";import{DisclosureGroup as H,Disclosure as J,Button as L,DisclosurePanel as Q}from"react-aria-components";import{cn as r}from"../../utils/cn.js";import{DownArrow as y}from"../Icon/DownArrow.js";function E(p){return null}function C({children:p,selectionMode:N="single",defaultSelectedKeys:w,defaultExpandedKeys:k,selectedKeys:A,onSelectionChange:I,disallowEmptySelection:v=!1,hideIndicator:D=!1,showDivider:S=!0,dividerProps:K,variant:i,isCompact:s,className:F,itemClasses:n}){const c=V.toArray(p).filter(_),b=(t,l)=>String(t.key??l).replace(/^\.\$/,"").replace(/=2/g,":").replace(/=0/g,"="),j=c.map(b),h=t=>t==="all"?j:t?[...t].map(String):void 0,P=h(w??k)??[],W=h(A);return e(H,{allowsMultipleExpanded:N==="multiple",defaultExpandedKeys:P,expandedKeys:W,onExpandedChange:t=>{const l=new Set([...t].map(String));v&&l.size===0||I?.(l)},className:r("px-0",i==="bordered"&&["px-4 py-4 rounded-xl border-2 border-border-subtle"],i==="shadow"&&["px-4 py-4 rounded-xl bg-surface-subtle border border-border-subtle"],F),children:c.map((t,l)=>{const f=b(t,l),{title:$,subtitle:m,startContent:g,indicator:u,hideIndicator:z,disableIndicatorAnimation:x,isDisabled:B,classNames:o,onPress:G,children:M}=t.props;return d(q,{children:[e(J,{id:f,isDisabled:B,className:r("w-full py-2","data-disabled:pointer-events-none data-disabled:opacity-50",i==="bordered"&&["rounded-xl py-0"],i==="splitted"&&["flex flex-col bg-surface-subtle rounded-xl border border-border-subtle"],s&&"py-0",n?.base,o?.base),children:({isExpanded:a})=>d(O,{children:[e("h2",{"data-open":a||void 0,className:r(n?.heading,o?.heading),children:d(L,{slot:"trigger","data-open":a||void 0,onPress:G,"aria-label":t.props["aria-label"],className:r("w-full h-full flex items-center gap-3 py-0 appearance-none cursor-pointer select-none","border-0 focus-visible:outline-2 focus-visible:outline focus-visible:outline-border-subtle","bg-transparent text-inherit",s&&["py-2"],n?.trigger,o?.trigger),children:[g!=null&&e("span",{className:r("shrink-0",n?.startContent,o?.startContent),children:g}),d("span",{className:r("flex-1 text-left",n?.titleWrapper,o?.titleWrapper),children:[e("span",{className:r("block text-xl font-semibold",s&&"text-base",n?.title,o?.title),children:$}),m!=null&&e("span",{className:r("block text-base",s&&"text-sm",n?.subtitle,o?.subtitle),children:m})]}),z||D?null:e("span",{"aria-hidden":"true","data-open":a||void 0,className:r("shrink-0",!x&&"transition-transform duration-200",!x&&a&&"-rotate-180",n?.indicator,o?.indicator),children:typeof u=="function"?e(u,{isOpen:a}):u??e(y,{className:"w-4 h-4"})})]})}),e("div",{className:r("grid transition-[grid-template-rows] duration-200 ease-in-out",a?"grid-rows-[1fr]":"grid-rows-[0fr]"),children:e("div",{className:"overflow-hidden min-h-0",children:e(Q,{className:r(s?"py-1":"py-2",n?.content,o?.content),children:M})})})]})}),S&&l<c.length-1&&e("hr",{className:r("border-solid border-b-0 border-x-0",K?.className??["my-2 border-border-subtle",i==="shadow"&&"border-border"])})]},f)})})}function ee(){return e(y,{className:"w-4 h-4"})}export{C as Accordion,E as AccordionItem,ee as AccordionItemArrow,C as default};
1
+ "use client";import{jsx as e,jsxs as b,Fragment as G}from"react/jsx-runtime";import{Children as V,isValidElement as q,Fragment as H,useState as D,useEffect as J}from"react";import{DisclosureGroup as Q,Disclosure as R,Button as U,DisclosurePanel as X}from"react-aria-components";import{cn as r}from"../../utils/cn.js";import{DownArrow as M}from"../Icon/DownArrow.js";const Y=200;function oe(n){return null}function le({children:n,selectionMode:f="single",defaultSelectedKeys:m,defaultExpandedKeys:h,selectedKeys:d,onSelectionChange:c,disallowEmptySelection:g=!1,keepContentMounted:y=!1,hideIndicator:s=!1,showDivider:K=!0,dividerProps:P,variant:u,isCompact:p,className:T,itemClasses:o}){const x=V.toArray(n).filter(q),w=(t,a)=>String(t.key??a).replace(/^\.\$/,"").replace(/=2/g,":").replace(/=0/g,"="),F=x.map(w),k=t=>t==="all"?F:t?[...t].map(String):void 0,W=k(m??h)??[],j=k(d);return e(Q,{allowsMultipleExpanded:f==="multiple",defaultExpandedKeys:W,expandedKeys:j,onExpandedChange:t=>{const a=new Set([...t].map(String));g&&a.size===0||c?.(a)},className:r("px-0",u==="bordered"&&["px-4 py-4 rounded-xl border-2 border-border-subtle"],u==="shadow"&&["px-4 py-4 rounded-xl bg-surface-subtle border border-border-subtle"],T),children:x.map((t,a)=>{const A=w(t,a),{title:L,subtitle:S,startContent:I,indicator:N,hideIndicator:O,disableIndicatorAnimation:v,isDisabled:_,keepContentMounted:$,classNames:l,onPress:z,children:B}=t.props;return b(H,{children:[e(R,{id:A,isDisabled:_,className:r("w-full py-2","data-disabled:pointer-events-none data-disabled:opacity-50",u==="bordered"&&["rounded-xl py-0"],u==="splitted"&&["flex flex-col bg-surface-subtle rounded-xl border border-border-subtle"],p&&"py-0",o?.base,l?.base),children:({isExpanded:i})=>b(G,{children:[e("h2",{"data-open":i||void 0,className:r(o?.heading,l?.heading),children:b(U,{slot:"trigger","data-open":i||void 0,onPress:z,"aria-label":t.props["aria-label"],className:r("w-full h-full flex items-center gap-3 py-0 appearance-none cursor-pointer select-none","border-0 focus-visible:outline-2 focus-visible:outline focus-visible:outline-border-subtle","bg-transparent text-inherit",p&&["py-2"],o?.trigger,l?.trigger),children:[I!=null&&e("span",{className:r("shrink-0",o?.startContent,l?.startContent),children:I}),b("span",{className:r("flex-1 text-left",o?.titleWrapper,l?.titleWrapper),children:[e("span",{className:r("block text-xl font-semibold",p&&"text-base",o?.title,l?.title),children:L}),S!=null&&e("span",{className:r("block text-base",p&&"text-sm",o?.subtitle,l?.subtitle),children:S})]}),O||s?null:e("span",{"aria-hidden":"true","data-open":i||void 0,className:r("shrink-0",!v&&"transition-transform duration-200",!v&&i&&"-rotate-180",o?.indicator,l?.indicator),children:typeof N=="function"?e(N,{isOpen:i}):N??e(M,{className:"w-4 h-4"})})]})}),e(Z,{isExpanded:i,keepContentMounted:$??y,className:r(p?"py-1":"py-2",o?.content,l?.content),children:B})]})}),K&&a<x.length-1&&e("hr",{className:r("border-solid border-b-0 border-x-0",P?.className??["my-2 border-border-subtle",u==="shadow"&&"border-border"])})]},A)})})}function Z({isExpanded:n,keepContentMounted:f,className:m,children:h}){const[d,c]=D(!1),[g,y]=D(n);return g!==n&&(y(n),c(!n)),J(()=>{if(!d)return;const s=setTimeout(()=>c(!1),Y+50);return()=>clearTimeout(s)},[d]),e("div",{className:r("grid transition-[grid-template-rows] duration-200 ease-in-out",n?"grid-rows-[1fr]":"grid-rows-[0fr]"),onTransitionEnd:s=>{s.target===s.currentTarget&&!n&&c(!1)},children:e("div",{className:"overflow-hidden min-h-0",children:e(X,{className:m,children:f||n||d?h:null})})})}function ae(){return e(M,{className:"w-4 h-4"})}export{le as Accordion,oe as AccordionItem,ae as AccordionItemArrow,le as default};
@@ -1 +1 @@
1
- "use client";import{jsxs as o,jsx as e}from"react/jsx-runtime";import{cn as n}from"../../utils/cn.js";function c({className:t,size:r=16}){return o("svg",{width:r,height:r,viewBox:"0 0 24 24",fill:"none",xmlns:"http://www.w3.org/2000/svg",className:n("text-ffl-gray-500 flex-shrink-0",t),stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e("path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"}),e("circle",{cx:"12",cy:"7",r:"4"})]})}export{c as UserIcon,c as default};
1
+ "use client";import{jsxs as t,jsx as e}from"react/jsx-runtime";import{cn as n}from"../../utils/cn.js";function c({className:o,size:r=16}){return t("svg",{width:r,height:r,viewBox:"0 0 24 24",fill:"none",xmlns:"http://www.w3.org/2000/svg",className:n("flex-shrink-0",o),stroke:"currentColor",strokeWidth:"2",strokeLinecap:"round",strokeLinejoin:"round",children:[e("path",{d:"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"}),e("circle",{cx:"12",cy:"7",r:"4"})]})}export{c as UserIcon,c as default};
@@ -13,7 +13,7 @@ import { ModalOverlayProps } from './ModalOverlay.types';
13
13
  * @clicktap/ui copies installed (ordinary pnpm version skew) the trigger wrote `visible` into one
14
14
  * copy while the overlay read the other's `unmounted` default — `animate="unmounted"` names no
15
15
  * variant, framer-motion holds `initial: "hidden"`, and every trigger-driven modal sat mounted at
16
- * `opacity: 0` with zero errors (CTAP-1210, noco). react-aria's context is the safe dependency:
16
+ * `opacity: 0` with zero errors (CTAP-1210, found by a consumer). react-aria's context is the safe dependency:
17
17
  * if react-aria-components itself is duplicated, everything breaks loudly.
18
18
  *
19
19
  * Exported for unit tests only.
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Date:** 2026-06-17
4
4
  **Package:** `@clicktap/ui`
5
- **Affects:** Any app consuming `@clicktap/ui`'s `Select` / `DropdownSelect` (Crispi, FFL, and any fork that renders pickers, sort controls, country/region selects, or configurable-product options).
5
+ **Affects:** Any app consuming `@clicktap/ui`'s `Select` / `DropdownSelect` (downstream projects and any fork that renders pickers, sort controls, country/region selects, or configurable-product options).
6
6
  **Classification:** FEATURE + BREAKING
7
7
  **Breaking:** Yes — (1) `DropdownSelect` removed, (2) the bare `<Select>` is now a **button picker**, not a combobox, (3) `DropdownSelect`'s `classNames.trigger` is now `classNames.input`, (4) the root is `w-full` by default, (5) `<Option>` is react-stately's `Item` — keyed by `key` (no `id`) and **cannot be wrapped**.
8
8
  **Formerly named:** `2026-06-17-select-standardization.md` — renamed 2026-08-10 (CTAP-1214) for global filename uniqueness; library guides carry their package slug so they can never collide with an app-level guide in a consumer's aggregated working set. **If you carry a `-applied` sibling under the old name, rename it to match — do NOT re-apply.**
@@ -1,7 +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
+ **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 a consumer.
5
5
  **Package:** `@clicktap/ui`
6
6
  **Breaking:** No — additive to the `exports` map. Existing `moduleResolution: node` / `node10`
7
7
  consumers are unaffected (that resolver ignores `exports`); this only *adds* working resolution under
@@ -50,7 +50,7 @@ emits only `.d.ts` for it).
50
50
  The original map declared only `types` + `import` conditions. That covers bundlers — but **Node CJS
51
51
  resolution** (what Jest uses without ESM flags) matches neither, so it fell through to the `.d.ts`
52
52
  and died on `export interface`. Any consumer whose Jest suite touches `useCooldown` (i.e. anyone
53
- adopting the CTAP-1114 `useResendCode` pattern *with* its tests) hit this; noco worked around it with
53
+ adopting the CTAP-1114 `useResendCode` pattern *with* its tests) hit this; one consumer worked around it with
54
54
  a `moduleNameMapper` straight to the `.js` files.
55
55
 
56
56
  Since this revision every runtime subpath entry carries a **`default` condition (last)** pointing at
@@ -12,7 +12,7 @@
12
12
  CTAP-1158 validated every publishable lib manifest against its real imports. For `@clicktap/ui`
13
13
  that audit changed the peer contract and corrected a wrong dependency pin, but no guide shipped
14
14
  with it — consumers would meet the changes as unexplained manifest churn on the next release
15
- (reported by family-rpg, 2026-08-07). This is that guide.
15
+ (reported by a consumer, 2026-08-07). This is that guide.
16
16
 
17
17
  ## What changed
18
18
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Date:** 2026-08-11
4
4
  **Revised:** 2026-08-11 — consumer action now says to rename a component named for the press-and-hold
5
- gesture (family-rpg's `HoldToRevealButton`), not only to swap its handlers.
5
+ gesture (for example a `HoldToRevealButton`), not only to swap its handlers.
6
6
  **Package:** `@clicktap/ui`
7
7
  **Affects:** `@clicktap/ui` — `Input` with `type="password"`.
8
8
  **Classification:** IMPROVEMENT (UX / accessibility)
@@ -69,7 +69,7 @@ Shipped in `@clicktap/ui` **0.43.6**.
69
69
  and add `aria-pressed` + the state-dependent `aria-label` while you are in there. The reference
70
70
  frontend also updated its one bespoke reveal (`UpdateEmailForm`) to match.
71
71
  - **Rename the component if its name describes the gesture.** If you named it for press-and-hold
72
- when you applied CTAP-1128 — family-rpg had `HoldToRevealButton` (2026-08-11) — the name now
72
+ when you applied CTAP-1128 — one consumer had `HoldToRevealButton` (2026-08-11) — the name now
73
73
  documents behaviour the code no longer implements, in the one place nobody re-reads. Rename it
74
74
  (`PasswordRevealButton`, `ShowPasswordToggle`) in the same change as the handler swap, and grep
75
75
  for the old name in tests and stories.
@@ -57,7 +57,7 @@ rather than as an error.
57
57
  ### When the duplicate is transitive — nothing of yours to pin
58
58
 
59
59
  The paragraph above assumes the second copy comes from a declaration you own. Often it does not:
60
- crispi resolves `react-stately` 3.49.0 cleanly across every workspace member and still carries a
60
+ one consumer resolves `react-stately` 3.49.0 cleanly across every workspace member and still carries a
61
61
  second copy at 3.46.0, arriving via `@adobe/react-spectrum` → `react-aria-components`. No manifest of
62
62
  theirs names it, so there is no pin to correct.
63
63
 
@@ -4,7 +4,7 @@
4
4
  **Package:** `@clicktap/ui`
5
5
  **Affects:** every controlled or default-expanded `Accordion` — checkout payment method selection, checkout's `defaultExpandedKeys={['cart']}`, totals/discount panel, PDP mobile overview, filter groups.
6
6
  **Classification:** BUGFIX
7
- **Breaking:** No — unless you shipped a workaround that pre-prefixes keys (see crispi note).
7
+ **Breaking:** No — unless you shipped a workaround that pre-prefixes keys (see the shim note below).
8
8
  **Ticket:** CTAP-1404
9
9
 
10
10
  ## The defect (2.0.x regression from the CTAP-1383 react-aria rewrite)
@@ -17,11 +17,11 @@ were only `String()`ed, so:
17
17
  1. `selectedKeys` / `defaultSelectedKeys` / `defaultExpandedKeys` never matched
18
18
  any item — nothing opened, nothing highlighted. The checkout payment step's
19
19
  method selection "not working" was this.
20
- 2. `onSelectionChange` handed the PREFIXED id back out — crispi's checkout
20
+ 2. `onSelectionChange` handed the PREFIXED id back out — a consumer's checkout
21
21
  dispatched `".$CYBERSOURCE"` as a payment method code to the backend.
22
22
 
23
23
  The heroui accordion this replaced handled keys correctly. Diagnosed by the
24
- crispi session against the installed 2.0.6 bundle.
24
+ consumer's session against the installed 2.0.6 bundle.
25
25
 
26
26
  ## The fix
27
27
 
@@ -36,13 +36,13 @@ round-trips with no output translation. `"all"` and unkeyed children (positional
36
36
 
37
37
  1. Take the release; no call-site changes needed — controlled accordions start
38
38
  working as written.
39
- 2. **Delete compensating shims.** crispi: `libs/crispi-ui/.../Accordion.tsx`
40
- wrapper (crispi-monorepo `7a44704f0`) must be REMOVED when taking this
39
+ 2. **Delete compensating shims.** A consumer-side `.../Accordion.tsx`
40
+ wrapper that pre-prefixes keys must be REMOVED when taking this
41
41
  release — it translates keys into the prefixed space, which no longer
42
42
  exists, so keeping it reintroduces the bug in the other direction. Check the
43
43
  shim's own escape handling is not needed elsewhere (this fix also covers
44
44
  escaped keys, which a bare `.$`-strip shim does not).
45
- 3. If any code path stored or transmitted a prefixed id (`.$…`) — crispi saw
45
+ 3. If any code path stored or transmitted a prefixed id (`.$…`) — one consumer saw
46
46
  them reach the backend as payment codes — check persisted data for the
47
47
  `.$` prefix.
48
48
 
@@ -28,7 +28,7 @@ observed:
28
28
  keyframe resolvers synchronously on the detached element.
29
29
 
30
30
  🚨 Thrown inside framer-motion's frame loop, so nothing reaches server logs and no React error
31
- boundary catches it. noco hit it in the warranty admin: a controlled drawer (route 1) containing a
31
+ boundary catches it. A consumer hit it in its warranty admin: a controlled drawer (route 1) containing a
32
32
  never-opened trigger-driven Page Builder overlay threw **13 errors on every close**, and in
33
33
  production that broke the frame loop — the close animation and the success toast were both lost.
34
34
  Every other trigger-driven overlay in that app (delete confirms, edit modals) carried the same
@@ -49,8 +49,8 @@ already gets from `AnimatePresence` is now the same on both paths:
49
49
 
50
50
  ## Consumer impact
51
51
 
52
- - **Nothing to change.** Take the release. If you gated the overlay yourself in a wrapper (noco
53
- tried it in `@noco/ui`), **remove that gate**: mounting only once already open, with the hook
52
+ - **Nothing to change.** Take the release. If you gated the overlay yourself in a wrapper (one
53
+ consumer tried it in its own UI library), **remove that gate**: mounting only once already open, with the hook
54
54
  initialized to `visible`, gives framer-motion no transition to play and leaves the backdrop stuck
55
55
  at `opacity: 0`. The fix has to live where the animation state is derived.
56
56
  - ⚠️ Do not read the mount as a signal. Nothing should depend on a closed trigger-driven overlay
@@ -5,11 +5,11 @@
5
5
  **Affects:** `src/components/PinInput/PinInput.tsx`, `PinInput.types.ts`. Every consumer of `<PinInput>` (signup, forgot-password and email/password-change verification on the storefront).
6
6
  **Classification:** BUGFIX (three) + IMPROVEMENT (`value`/`name` honored)
7
7
  **Breaking:** No API change. **Behavioral:** passing `value` now controls the boxes (it was silently ignored); `maxLength` is no longer rendered; `autoComplete="one-time-code"` is on the first box only.
8
- **Ticket:** CTAP-1749 (from NOCOE-2944)
8
+ **Ticket:** CTAP-1749
9
9
 
10
10
  ## What was wrong
11
11
 
12
- Four things, all in this component (a consumer wrapper such as noco's `libs/noco-ui` only restyles it):
12
+ Four things, all in this component (a consumer wrapper in a consumer's UI library only restyles it):
13
13
 
14
14
  1. **Android autofill and Gboard's clipboard chip lost five of six digits.** The paste handler split
15
15
  digits across the boxes correctly, but autofill and the keyboard chip insert text as an *input*
@@ -0,0 +1,25 @@
1
+ # `UserIcon` no longer carries a project's color class
2
+
3
+ **Date:** 2026-10-02
4
+ **Package:** `@clicktap/ui`
5
+ **Affects:** `components/Icon/UserIcon`
6
+ **Classification:** IMPROVEMENT
7
+ **Breaking:** No for any app that does not define the removed color token. One that does loses that color on the icon until it passes the class itself.
8
+ **Ticket:** CTAP-2094
9
+
10
+ ## What changed
11
+
12
+ `UserIcon` hard-coded a `text-*-gray-500` class for a color token one consuming project defines in its
13
+ own theme. The platform defines no such token, so the class did nothing here, and it named a project in
14
+ the shared library. The icon now carries only `flex-shrink-0` and inherits its color like the other
15
+ icons, so a consumer sets the color it wants through `className`.
16
+
17
+ ## Migrate
18
+
19
+ If your theme defined that token (or any color you relied on for this icon), pass it:
20
+
21
+ ```tsx
22
+ <UserIcon className="text-brand-gray-500" />
23
+ ```
24
+
25
+ Nothing else.
@@ -0,0 +1,93 @@
1
+ # `Accordion` mounts a collapsed item's content only when it expands
2
+
3
+ **Date:** 2026-10-06
4
+ **Package:** `@clicktap/ui`
5
+ **Affects:** `components/Accordion` (`Accordion`, `AccordionItem`)
6
+ **Classification:** BUGFIX
7
+ **Breaking:** No API change. ⚠️ Behavior change for any consumer that relies on collapsed content being in the server-rendered HTML, findable with the browser's find-in-page, or keeping its state across a collapse. Pass `keepContentMounted` there.
8
+ **Ticket:** CTAP-2217
9
+
10
+ ## Dependencies
11
+
12
+ *None*
13
+
14
+ ## Summary
15
+
16
+ The react-aria `Accordion` that replaced heroui's in `@clicktap/ui` 2.0 rendered every item's content,
17
+ expanded or not, and hid collapsed items with CSS only. Collapsed content mounted, ran its effects and
18
+ queries, and filled the DOM. One consumer's page, with a data-fetching panel per group and nested
19
+ accordions, sent 520 GraphQL requests and rendered about 72,000 hidden DOM nodes on load. heroui never
20
+ mounted a collapsed item unless `keepContentMounted` was set, and `AccordionProps` kept declaring that
21
+ prop, but the component ignored it.
22
+
23
+ The default is now heroui's: a collapsed item's content is not rendered. It mounts when the item expands,
24
+ stays mounted while the collapse transition runs, then unmounts. `keepContentMounted` works again.
25
+
26
+ ## Changes
27
+
28
+ | | Before | After |
29
+ | --- | --- | --- |
30
+ | Collapsed item, default | Content mounted, hidden by CSS | Content not rendered |
31
+ | Expanding | Already mounted | Content mounts |
32
+ | Collapsing | Stays mounted | Unmounts once the height transition ends (or after 250 ms) |
33
+ | `keepContentMounted` on `Accordion` | Accepted, ignored | Every item's content always mounted, as before this fix |
34
+ | `keepContentMounted` on `AccordionItem` | Not a prop | Overrides the `Accordion`'s value for that item, in either direction |
35
+
36
+ The panel element itself (`DisclosurePanel`, the target of the trigger's `aria-controls`) is still always
37
+ rendered; only its children are conditional.
38
+
39
+ **Source**: `libs/frontend-modules/ui/src/components/Accordion/Accordion.tsx` (`AccordionPanel`)
40
+
41
+ ## Migrate
42
+
43
+ Most accordions need nothing: menus, filters, cookie categories and checkout sections are interactive
44
+ content that nobody needs before it is opened. Pass `keepContentMounted` where one of these holds.
45
+
46
+ 1. **The content must be in the initial HTML**, typically for search engines: FAQ answers, product
47
+ details, specifications.
48
+
49
+ ```tsx
50
+ <Accordion keepContentMounted>
51
+ {faqs.map((faq) => (
52
+ <AccordionItem key={faq.id} title={faq.question}>
53
+ {faq.answer}
54
+ </AccordionItem>
55
+ ))}
56
+ </Accordion>
57
+ ```
58
+
59
+ The platform's reference storefront does this in `apps/frontend/components/FAQ/FAQ.tsx`.
60
+
61
+ 2. **State inside the panel must survive a collapse**: a half-filled form, a payment widget that
62
+ initializes once, a nested accordion's open item. Set it on the whole accordion or on just that
63
+ item:
64
+
65
+ ```tsx
66
+ <Accordion>
67
+ <AccordionItem key="card" title="Card" keepContentMounted>
68
+ <CardForm />
69
+ </AccordionItem>
70
+ <AccordionItem key="paypal" title="PayPal">
71
+ <PayPalButton />
72
+ </AccordionItem>
73
+ </Accordion>
74
+ ```
75
+
76
+ An item can also opt out of a group-level `keepContentMounted` with `keepContentMounted={false}`.
77
+
78
+ 3. **Code that reads collapsed content from the DOM** (a test that queries a collapsed item's text, a
79
+ script that measures it) must expand the item first, or the accordion must keep its content mounted.
80
+
81
+ Find your call sites:
82
+
83
+ ```bash
84
+ grep -rn '<Accordion\b' apps/ libs/ --include=*.tsx
85
+ ```
86
+
87
+ ## Verify
88
+
89
+ - With the default, open a page with an accordion and search the page source (view-source, not the
90
+ inspector) for text from a collapsed item: it is absent. Expanding the item renders it.
91
+ - With `keepContentMounted`, the same text is present in the page source.
92
+ - On a page whose panels fetch data, the network panel shows no requests for collapsed items until they
93
+ are opened.
@@ -1,4 +1,24 @@
1
1
  [
2
+ {
3
+ "path": "2026/10/ui-2026-10-06-accordion-mounts-collapsed-content-on-expand.md",
4
+ "module": "libs/frontend-modules/ui",
5
+ "package": "@clicktap/ui",
6
+ "date": "2026-10-06",
7
+ "title": "`Accordion` mounts a collapsed item's content only when it expands",
8
+ "classification": "BUGFIX",
9
+ "breaking": "No API change. ⚠️ Behavior change for any consumer that relies on collapsed content being in the server-rendered HTML, findable with the browser's find-in-page, or keeping its state across a collapse. Pass `keepContentMounted` there.",
10
+ "affects": "`components/Accordion` (`Accordion`, `AccordionItem`)"
11
+ },
12
+ {
13
+ "path": "2026/10/ui-2026-10-02-user-icon-drops-project-color-class.md",
14
+ "module": "libs/frontend-modules/ui",
15
+ "package": "@clicktap/ui",
16
+ "date": "2026-10-02",
17
+ "title": "`UserIcon` no longer carries a project's color class",
18
+ "classification": "IMPROVEMENT",
19
+ "breaking": "No for any app that does not define the removed color token. One that does loses that color on the icon until it passes the class itself.",
20
+ "affects": "`components/Icon/UserIcon`"
21
+ },
2
22
  {
3
23
  "path": "2026/09/ui-2026-09-30-modal-fade-no-final-frame-blink.md",
4
24
  "module": "libs/frontend-modules/ui",
@@ -156,7 +176,7 @@
156
176
  "date": "2026-08-24",
157
177
  "title": "Accordion controlled selection matches authored keys again",
158
178
  "classification": "BUGFIX",
159
- "breaking": "No — unless you shipped a workaround that pre-prefixes keys (see crispi note).",
179
+ "breaking": "No — unless you shipped a workaround that pre-prefixes keys (see the shim note below).",
160
180
  "affects": "every controlled or default-expanded `Accordion` — checkout payment method selection, checkout's `defaultExpandedKeys={['cart']}`, totals/discount panel, PDP mobile overview, filter groups."
161
181
  },
162
182
  {
@@ -393,7 +413,7 @@
393
413
  "breaking": "No — additive to the `exports` map. Existing `moduleResolution: node` / `node10`",
394
414
  "affects": "",
395
415
  "revised": "2026-08-08",
396
- "revised_note": "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."
416
+ "revised_note": "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 a consumer."
397
417
  },
398
418
  {
399
419
  "path": "2026/06/ui-2026-06-18-select-hiddenselect-setvalue-crash.md",
@@ -413,7 +433,7 @@
413
433
  "title": "Select standardization — one `<Select searchable>`, `DropdownSelect` removed",
414
434
  "classification": "FEATURE + BREAKING",
415
435
  "breaking": "Yes — (1) `DropdownSelect` removed, (2) the bare `<Select>` is now a **button picker**, not a combobox, (3) `DropdownSelect`'s `classNames.trigger` is now `classNames.input`, (4) the root is `w-full` by default, (5) `<Option>` is react-stately's `Item` — keyed by `key` (no `id`) and **cannot be wrapped**.",
416
- "affects": "Any app consuming `@clicktap/ui`'s `Select` / `DropdownSelect` (Crispi, FFL, and any fork that renders pickers, sort controls, country/region selects, or configurable-product options)."
436
+ "affects": "Any app consuming `@clicktap/ui`'s `Select` / `DropdownSelect` (downstream projects and any fork that renders pickers, sort controls, country/region selects, or configurable-product options)."
417
437
  },
418
438
  {
419
439
  "path": "2026/05/ui-2026-05-29-password-check-requirements-variant.md",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clicktap/ui",
3
- "version": "2.4.3",
3
+ "version": "2.4.5",
4
4
  "private": false,
5
5
  "author": "Clicktap",
6
6
  "description": "A library of React UI components and low-level hooks.",