@clicktap/ui 2.0.7 → 2.0.9

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,3 +1,20 @@
1
1
  import { BadgeProps } from './Badge.types';
2
+ /**
3
+ * ⚠️ Slot map — `classNames.badge` is the PILL (CTAP-1491).
4
+ *
5
+ * `@heroui/badge` rendered two elements, and its single badge element carried BOTH the absolute
6
+ * positioning and the whole visual treatment, so `classNames.badge` reached the thing that draws
7
+ * the badge. CTAP-1383 replaced it with this component, which splits that element in two — a
8
+ * positioning span and an animated pill — and the slot stayed on the positioning half. The pill
9
+ * then had no slot at all, so a consumer restyling "the badge" painted and sized a transparent
10
+ * wrapper while the pill kept its `variant` styling underneath: a coloured square behind the pill.
11
+ *
12
+ * Applying `badge` to both halves is not the fix — that is what paints the wrapper. So each element
13
+ * has its own slot:
14
+ *
15
+ * base the relative anchor (unchanged)
16
+ * wrapper the absolutely-positioned span — placement and offset only
17
+ * badge the visible pill — colour, size, radius, typography
18
+ */
2
19
  export declare function Badge({ children, isInvisible, disableAnimation, placement, shape, className, classNames, showOutline, content, isDot, isOneChar, size, variant, ...props }: BadgeProps): import("react/jsx-runtime").JSX.Element;
3
20
  export default Badge;
@@ -1 +1 @@
1
- "use client";import{jsxs as V,jsx as n}from"react/jsx-runtime";import{useId as v}from"react";import{AnimatePresence as z,m as C}from"framer-motion";import{cn as a}from"../../utils/cn.js";const S={hidden:{opacity:0,scale:0},show:{opacity:1,scale:1}};function P({children:h,isInvisible:f,disableAnimation:l,placement:t="top-right",shape:e="rectangle",className:g,classNames:d,showOutline:c=!1,content:o,isDot:i,isOneChar:x,size:s="md",variant:r="solid",...b}){const m=v(),u=l?{duration:0}:{type:"spring",bounce:.3},w=String(o)?.length===1||x,p={sm:"text-xs",md:"text-sm",lg:"text-sm"},y=w?{sm:"w-4 h-4",md:"w-5 h-5",lg:"w-6 h-6"}[s]:{sm:"min-w-4 h-4",md:"min-w-5 h-5",lg:"min-w-6 h-6"}[s];return V("div",{...b,className:a("relative","inline-flex shrink-0",g,d?.base),children:[h,n("span",{className:a("p-0","border-0","absolute z-10","flex",[t==="top-right"&&e==="rectangle"&&"top-0.5 right-px",t==="top-right"&&e==="circle"&&"top-1 right-1",t==="bottom-right"&&e==="rectangle"&&"bottom-0.5 right-px",t==="bottom-right"&&e==="circle"&&"bottom-1 right-1",t==="top-left"&&e==="rectangle"&&"top-0.5 left-px",t==="top-left"&&e==="circle"&&"top-1 left-1",t==="bottom-left"&&e==="rectangle"&&"bottom-0.5 left-px",t==="bottom-left"&&e==="circle"&&"bottom-1 left-1"],[t==="top-right"&&"translate-x-1/2 -translate-y-1/2",t==="bottom-right"&&"translate-x-1/2 translate-y-1/2",t==="bottom-left"&&"-translate-x-1/2 translate-y-1/2",t==="top-left"&&"-translate-x-1/2 -translate-y-1/2"],d?.badge),children:n(z,{children:!f&&n(C.span,{layout:!0,variants:S,initial:l?"show":"hidden",transition:u,animate:"show",exit:"hidden",className:a("flex flex-wrap","place-content-center","z-10","box-border","rounded-full","py-0 px-1","font-normal text-fg","select-none","whitespace-normal",r==="faded"?"text-slate-800":"text-white",!o||i?"w-3.5 h-3.5":y,typeof o=="string"&&o.length>1||i?"text-xs":p[s],[r==="solid"&&"bg-surface-inverse-hover",r==="shadow"&&"bg-surface-inverse-hover",r==="flat"&&"bg-surface-inverse-hover/55",r==="faded"&&"bg-white"],[r==="faded"&&"border-2 border-solid border-surface-inverse-hover",c&&r!=="faded"&&"border-2 border-solid border-white"]),children:!i&&o},`badge-${m}`)})})]})}export{P as Badge,P as default};
1
+ "use client";import{jsxs as V,jsx as a}from"react/jsx-runtime";import{useId as v}from"react";import{AnimatePresence as z,m as C}from"framer-motion";import{cn as l}from"../../utils/cn.js";const S={hidden:{opacity:0,scale:0},show:{opacity:1,scale:1}};function P({children:h,isInvisible:f,disableAnimation:d,placement:t="top-right",shape:e="rectangle",className:g,classNames:i,showOutline:c=!1,content:o,isDot:s,isOneChar:x,size:n="md",variant:r="solid",...b}){const m=v(),p=d?{duration:0}:{type:"spring",bounce:.3},w=String(o)?.length===1||x,u={sm:"text-xs",md:"text-sm",lg:"text-sm"},y=w?{sm:"w-4 h-4",md:"w-5 h-5",lg:"w-6 h-6"}[n]:{sm:"min-w-4 h-4",md:"min-w-5 h-5",lg:"min-w-6 h-6"}[n];return V("div",{...b,className:l("relative","inline-flex shrink-0",g,i?.base),children:[h,a("span",{className:l("p-0","border-0","absolute z-10","flex",[t==="top-right"&&e==="rectangle"&&"top-0.5 right-px",t==="top-right"&&e==="circle"&&"top-1 right-1",t==="bottom-right"&&e==="rectangle"&&"bottom-0.5 right-px",t==="bottom-right"&&e==="circle"&&"bottom-1 right-1",t==="top-left"&&e==="rectangle"&&"top-0.5 left-px",t==="top-left"&&e==="circle"&&"top-1 left-1",t==="bottom-left"&&e==="rectangle"&&"bottom-0.5 left-px",t==="bottom-left"&&e==="circle"&&"bottom-1 left-1"],[t==="top-right"&&"translate-x-1/2 -translate-y-1/2",t==="bottom-right"&&"translate-x-1/2 translate-y-1/2",t==="bottom-left"&&"-translate-x-1/2 translate-y-1/2",t==="top-left"&&"-translate-x-1/2 -translate-y-1/2"],i?.wrapper),children:a(z,{children:!f&&a(C.span,{layout:!0,variants:S,initial:d?"show":"hidden",transition:p,animate:"show",exit:"hidden",className:l("flex flex-wrap","place-content-center","z-10","box-border","rounded-full","py-0 px-1","font-normal text-fg","select-none","whitespace-normal",r==="faded"?"text-slate-800":"text-white",!o||s?"w-3.5 h-3.5":y,typeof o=="string"&&o.length>1||s?"text-xs":u[n],[r==="solid"&&"bg-surface-inverse-hover",r==="shadow"&&"bg-surface-inverse-hover",r==="flat"&&"bg-surface-inverse-hover/55",r==="faded"&&"bg-white"],[r==="faded"&&"border-2 border-solid border-surface-inverse-hover",c&&r!=="faded"&&"border-2 border-solid border-white"],i?.badge),children:!s&&o},`badge-${m}`)})})]})}export{P as Badge,P as default};
@@ -4,7 +4,13 @@ import { HTMLAttributes, ReactNode } from 'react';
4
4
  * The heroui component only contributed two wrapper elements while every
5
5
  * visual class already lived here — importing it cost the whole @heroui
6
6
  * system (~25 KiB gz) in the initial bundle of every page via the header's
7
- * cart badge. Same prop names and defaults as before.
7
+ * cart badge.
8
+ *
9
+ * ⚠️ Prop names and defaults match the heroui version, but `classNames` does NOT map one-to-one and
10
+ * an earlier revision of this comment claimed it did. heroui rendered a single badge element that
11
+ * carried both positioning and appearance; this component splits it into a positioning wrapper and
12
+ * a pill, so there are three slots rather than two. `classNames.badge` still means the visible pill
13
+ * — see {@link BadgeProps.classNames} (CTAP-1491).
8
14
  */
9
15
  export type BadgeProps = Omit<HTMLAttributes<HTMLDivElement>, 'content'> & {
10
16
  children?: ReactNode;
@@ -15,8 +21,21 @@ export type BadgeProps = Omit<HTMLAttributes<HTMLDivElement>, 'content'> & {
15
21
  disableAnimation?: boolean;
16
22
  placement?: 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
17
23
  shape?: 'rectangle' | 'circle';
24
+ /**
25
+ * Per-element class slots.
26
+ *
27
+ * ⚠️ `badge` is the visible PILL, not the positioning wrapper. Between CTAP-1383 and CTAP-1491 it
28
+ * landed on the wrapper, which has no background or radius of its own — so classes meant for the
29
+ * pill painted and sized a transparent box around it instead. If you worked around that with
30
+ * child variants (`[&>span]:bg-…`), drop the `[&>span]:` prefix: those classes now belong on
31
+ * `badge` directly, and left as-is they would target a child of the pill that does not exist.
32
+ */
18
33
  classNames?: {
34
+ /** The relative anchor wrapping `children`. */
19
35
  base?: string;
36
+ /** The absolutely-positioned span — placement and offset only; has no visual styling. */
37
+ wrapper?: string;
38
+ /** The visible pill — colour, size, radius, typography. */
20
39
  badge?: string;
21
40
  };
22
41
  showOutline?: boolean;
@@ -0,0 +1,90 @@
1
+ # The build fails if it emits the development JSX runtime
2
+
3
+ **Date:** 2026-08-24
4
+ **Package:** `@clicktap/ui` (also `@clicktap/state`, `@clicktap/csx`)
5
+ **Affects:** anyone building or publishing these packages. Consumers receive a guarantee, not a change.
6
+ **Classification:** BUGFIX
7
+ **Breaking:** No. A correct build is unaffected; an incorrect one now fails instead of publishing.
8
+
9
+ ## Problem
10
+
11
+ A build could emit React's **development** JSX runtime — `jsxDEV(...)` call sites
12
+ plus a bundled `assets/jsx-dev-runtime-*.js` chunk. A consumer's production React
13
+ does not export `jsxDEV`, so the package compiles and then dies at prerender:
14
+
15
+ ```
16
+ TypeError: r.j.jsxDEV is not a function
17
+ at .next/server/app/page.js
18
+ Error occurred prerendering page "/"
19
+ ```
20
+
21
+ ⚠️ **Nothing in the build looked at the JSX runtime.** Not
22
+ `verifyUseClientDirectives`, not typecheck, not lint. The artifact was
23
+ structurally valid and publishable; only a consumer's `next build` found it.
24
+
25
+ ## It was never intermittent
26
+
27
+ CTAP-1393 recorded this as intermittent — seen twice, then not reproducible across
28
+ five clean builds. It is deterministic, and the trigger was in `project.json`:
29
+
30
+ ```json
31
+ "configurations": {
32
+ "development": { "mode": "development" },
33
+ "production": { "mode": "production" }
34
+ }
35
+ ```
36
+
37
+ `@vitejs/plugin-react` selects the dev JSX runtime from the resolved mode. So
38
+ `--configuration=development` — or any dependent target that propagated it —
39
+ produced a dev-runtime artifact, every time. The five "clean" builds used the
40
+ default production configuration.
41
+
42
+ Measured on this repo, and matching the original report's numbers exactly:
43
+
44
+ | build | emitted files referencing `jsxDEV` |
45
+ | --- | --- |
46
+ | default (production) | **0** |
47
+ | `--mode=development` | **206** |
48
+
49
+ ## Fix — three layers, and only one of them is load-bearing
50
+
51
+ 1. **The `development` build configuration is removed** from `ui`, `state`, `csx`
52
+ and `conversion-tracking-facebook`. A published library artifact has no
53
+ legitimate development mode, and nothing in the repo invoked it. This removes
54
+ the mechanism.
55
+ 2. **`verifyProductionJsxRuntime`** now runs in the same `closeBundle` family as
56
+ `verifyUseClientDirectives`, on `ui`, `state` and `csx`. It fails the build if
57
+ any emitted `.js` references `jsxDEV` or `jsx-dev-runtime`.
58
+ 3. It also asserts the **positive** direction: a package whose output references
59
+ React must show at least one `react/jsx-runtime` import. Checking only "no
60
+ jsxDEV" passes vacuously on a build that emitted nothing.
61
+
62
+ ⚠️ **(2) is the one that holds.** A configuration fix cannot cover a mode arriving
63
+ from a CLI flag, an inherited configuration on a dependent target, or a direct
64
+ vite invocation. The assertion looks at what was actually EMITTED, so it holds
65
+ however the mode was arrived at — which is the difference between making this
66
+ unlikely and making it impossible.
67
+
68
+ The positive check is conditioned on React output rather than required outright:
69
+ `csx` legitimately emits no components (two files, no React), and an assertion
70
+ that fails a correct build gets deleted.
71
+
72
+ ## Verified in both directions
73
+
74
+ ```
75
+ ui production 248 file(s) clean; 205 import react/jsx-runtime -> build passes
76
+ ui --mode=dev 206 emitted file(s) reference the DEV runtime -> build FAILS
77
+ state production 44 file(s) clean; 12 import react/jsx-runtime -> build passes
78
+ state --mode=dev 14 emitted file(s) reference the DEV runtime -> build FAILS
79
+ csx production 2 file(s) scanned; no React output to check -> build passes
80
+ ```
81
+
82
+ ## Migration checklist
83
+
84
+ - [ ] Nothing to do as a consumer.
85
+ - [ ] If you fork these packages and script a build with
86
+ `--configuration=development`, that configuration no longer exists — drop
87
+ the flag. It was producing an artifact that could not be published.
88
+ - [ ] If you add a new published frontend library, wire
89
+ `verifyProductionJsxRuntime(<its dist dir>)` into its vite plugins and do
90
+ not give it a `development` build configuration.
@@ -0,0 +1,116 @@
1
+ # BUGFIX: Badge `classNames.badge` targets the pill again, and the wrapper gets its own slot
2
+
3
+ **Date:** 2026-08-26
4
+ **Package:** `@clicktap/ui`
5
+ **Affects:** `src/components/Badge/Badge.tsx`, `Badge.types.ts`
6
+ **Classification:** BUGFIX
7
+ **Breaking:** ⚠️ Yes for anyone who worked around the CTAP-1383 behaviour — see Consumer Impact.
8
+ **Ticket:** CTAP-1491
9
+
10
+ ## Summary
11
+
12
+ `@heroui/badge` rendered **two** elements:
13
+
14
+ ```jsx
15
+ <div className={slots.base(classNames.base)}>
16
+ {children}
17
+ <Component {...getBadgeProps()}>{content}</Component> // slots.badge + classNames.badge
18
+ </div>
19
+ ```
20
+
21
+ That second element *was* the badge: it carried the absolute positioning **and** the whole visual
22
+ treatment. So `classNames.badge` reached the thing that draws the badge.
23
+
24
+ CTAP-1383 replaced heroui with an in-house Badge — worth doing, since importing heroui pulled ~25 KiB
25
+ gz into every page's initial bundle via the header's cart badge. But it splits that one element into
26
+ **three**:
27
+
28
+ ```
29
+ <div> base relative anchor
30
+ <span> — absolute positioning: placement, translate
31
+ <m.span> — the visible pill: rounded-full, bg, size, text, variant
32
+ ```
33
+
34
+ …and `classNames.badge` stayed on the **middle** one. The pill had no slot at all, so its appearance
35
+ was reachable only through `variant` and `size`.
36
+
37
+ ⚠️ A consumer restyling "the badge" therefore painted and sized a wrapper that has no background and
38
+ no `rounded-full` of its own, while the pill kept its `variant` styling underneath — rendering a
39
+ coloured square behind the pill.
40
+
41
+ ⚠️ **Nothing could catch this.** The prop still existed with the same name and type, so consumer code
42
+ compiled, typechecked and linted clean; only the runtime element differed. `Badge.types.ts` also
43
+ asserted "Same prop names and defaults as before", which was true of the signature and false of the
44
+ behaviour — that sentence is now corrected.
45
+
46
+ ## What changed
47
+
48
+ Three slots, one per element:
49
+
50
+ | slot | element | what belongs there |
51
+ |---|---|---|
52
+ | `base` | the relative anchor | unchanged |
53
+ | `wrapper` | **new** — the absolutely-positioned span | placement, offset |
54
+ | `badge` | the visible pill | colour, size, radius, typography |
55
+
56
+ ⚠️ Applying `badge` to both halves was considered and rejected — painting the wrapper is the bug.
57
+
58
+ `classNames.badge` is merged **last** on the pill, so `cn` (tailwind-merge) reconciles a consumer's
59
+ size or colour against the computed `sizeVariant`/`variant` classes rather than leaving the outcome
60
+ to specificity or stylesheet order.
61
+
62
+ ## Consumer Impact
63
+
64
+ **If you never passed `classNames` to a Badge, nothing changes.** No component in this repo does,
65
+ which is why the regression went unnoticed upstream in the first place.
66
+
67
+ **If you have pre-CTAP-1383 (heroui-era) code**, your `classNames.badge` starts working again with no
68
+ edit — that is the point of restoring the meaning.
69
+
70
+ ⚠️ **If you worked around CTAP-1383 with child variants, you must undo it.** The known shape is:
71
+
72
+ ```tsx
73
+ // BEFORE — targeting the pill through the wrapper
74
+ badge: cn(
75
+ 'top-px',
76
+ '[&>span]:bg-brand-red [&>span]:text-white [&>span]:border-0',
77
+ '[&>span]:min-w-5 [&>span]:h-5',
78
+ ),
79
+ ```
80
+
81
+ ```tsx
82
+ // AFTER — the pill has its own slot; positioning moves to `wrapper`
83
+ wrapper: 'top-px',
84
+ badge: cn(
85
+ 'bg-brand-red text-white border-0',
86
+ 'min-w-5 h-5',
87
+ ),
88
+ ```
89
+
90
+ Left unchanged, `[&>span]:` now targets a `span` child *of the pill*, which does not exist — so every
91
+ one of those declarations silently stops applying and the badge reverts to its `variant` styling.
92
+ ⚠️ Same failure shape as the original bug, so grep for `[&>span]:` near a Badge before upgrading.
93
+
94
+ That workaround was always brittle for a second reason worth retiring: it depended on the pill
95
+ remaining a direct `span` child of the wrapper, so any future wrapper element inside that span would
96
+ have detached it silently.
97
+
98
+ ## Verification
99
+
100
+ `src/components/Badge/Badge.spec.tsx` — 6 tests, the first exerciser `classNames` has had.
101
+
102
+ **Mutation-tested.** Reverting to the CTAP-1383 wiring fails **5 of 6**. The sixth (`base` on the
103
+ anchor) passes either way, correctly — `base` was never broken.
104
+
105
+ Pinned deliberately: `badge` lands on the pill; `badge` does **not** also land on the wrapper (a
106
+ positive-only assertion would pass if it were applied to both — the other wrong answer, and the one
107
+ that reproduces the coloured square); `wrapper` lands on the wrapper and not the pill; `base` on the
108
+ anchor only; the three slots resolve to three **different** elements (one element carrying all three
109
+ would satisfy each assertion taken alone); and a consumer's `h-6` beats the computed `h-5` with no
110
+ `h-5` left behind, which is what the merge-last ordering buys.
111
+
112
+ A `ClassNameSlots` story tints each slot so the mapping is visible in Storybook.
113
+
114
+ ⚠️ **Not verified visually against a real brand theme.** The tests assert which element each class
115
+ lands on, in jsdom. That the resulting badge *looks* right on a consumer's storefront is not
116
+ something this suite can show.
@@ -1,4 +1,34 @@
1
1
  [
2
+ {
3
+ "path": "2026/08/ui-2026-08-26-badge-classnames-badge-targets-the-pill.md",
4
+ "module": "libs/frontend-modules/ui",
5
+ "package": "@clicktap/ui",
6
+ "date": "2026-08-26",
7
+ "title": "BUGFIX: Badge `classNames.badge` targets the pill again, and the wrapper gets its own slot",
8
+ "classification": "BUGFIX",
9
+ "breaking": "⚠️ Yes for anyone who worked around the CTAP-1383 behaviour — see Consumer Impact.",
10
+ "affects": "`src/components/Badge/Badge.tsx`, `Badge.types.ts`"
11
+ },
12
+ {
13
+ "path": "2026/08/ui-2026-08-24-production-jsx-runtime-guard.md",
14
+ "module": "libs/frontend-modules/ui",
15
+ "package": "@clicktap/ui",
16
+ "date": "2026-08-24",
17
+ "title": "The build fails if it emits the development JSX runtime",
18
+ "classification": "BUGFIX",
19
+ "breaking": "No. A correct build is unaffected; an incorrect one now fails instead of publishing.",
20
+ "affects": "anyone building or publishing these packages. Consumers receive a guarantee, not a change."
21
+ },
22
+ {
23
+ "path": "2026/08/ui-2026-08-24-accordion-author-key-space.md",
24
+ "module": "libs/frontend-modules/ui",
25
+ "package": "@clicktap/ui",
26
+ "date": "2026-08-24",
27
+ "title": "Accordion controlled selection matches authored keys again",
28
+ "classification": "BUGFIX",
29
+ "breaking": "No — unless you shipped a workaround that pre-prefixes keys (see crispi note).",
30
+ "affects": "every controlled or default-expanded `Accordion` — checkout payment method selection, checkout's `defaultExpandedKeys={['cart']}`, totals/discount panel, PDP mobile overview, filter groups."
31
+ },
2
32
  {
3
33
  "path": "2026/08/ui-2026-08-23-use-client-propagates-to-barrels.md",
4
34
  "module": "libs/frontend-modules/ui",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clicktap/ui",
3
- "version": "2.0.7",
3
+ "version": "2.0.9",
4
4
  "private": false,
5
5
  "author": "Clicktap",
6
6
  "description": "A library of React UI components and low-level hooks.",