@clicktap/ui 2.3.0 → 2.3.2
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.
- package/components/Button/Button.js +1 -1
- package/components/Collection/CollectionFilters/CollectionFilters.js +1 -1
- package/components/Collection/EmptyResults/EmptyResults.js +1 -1
- package/components/Dialog/Dialog.js +1 -1
- package/components/Drawer/Drawer.d.ts +1 -1
- package/components/Drawer/Drawer.js +1 -1
- package/components/Drawer/Drawer.types.d.ts +7 -0
- package/components/ResendCode/ResendCode.js +1 -1
- package/components/Select/ComboBoxSelect.js +1 -1
- package/docs/migrations/2026/09/ui-2026-09-19-dialog-scrolls-within-viewport.md +32 -0
- package/docs/migrations/2026/09/ui-2026-09-23-drawer-viewport-clamp.md +90 -0
- package/docs/migrations/2026/09/ui-2026-09-23-interactive-elements-cursor-pointer.md +84 -0
- package/docs/migrations/index.json +30 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as f}from"react/jsx-runtime";import{forwardRef as p}from"react";import{Button as c}from"react-aria-components";import{cn as g}from"../../utils/cn.js";function h(t){switch(t){case"primary":return"solid";case"secondary":return"outline";case"tertiary":return"ghost";default:return t}}const w=p(function({className:s,isDisabled:n=!1,isLoading:e=!1,size:o="md",variant:
|
|
1
|
+
"use client";import{jsx as f}from"react/jsx-runtime";import{forwardRef as p}from"react";import{Button as c}from"react-aria-components";import{cn as g}from"../../utils/cn.js";function h(t){switch(t){case"primary":return"solid";case"secondary":return"outline";case"tertiary":return"ghost";default:return t}}const w=p(function({className:s,isDisabled:n=!1,isLoading:e=!1,size:o="md",variant:d="solid",...a},b){const r=h(d),u=["flex items-center justify-center","rounded-md border-solid border","font-semibold text-sm","py-2 px-8","transition-all duration-200 ease-in-out","cursor-pointer disabled:cursor-not-allowed","focus:outline-2 focus:outline focus:outline-border-subtle pressed:scale-95",o==="sm"&&"h-8",o==="md"&&"h-10",o==="lg"&&"h-12",r==="ghost"&&["bg-transparent hover:bg-transparent focus:bg-transparent disabled:bg-transparent","border-transparent hover:border-transparent focus:border-transparent disabled:border-transparent","text-fg disabled:text-fg-subtle",e&&"disabled:text-fg"],r==="link"&&["bg-transparent hover:bg-transparent focus:bg-transparent disabled:bg-transparent","border-transparent hover:border-transparent focus:border-transparent disabled:border-transparent","inline-flex h-auto p-0 rounded-none","underline underline-offset-2","text-fg disabled:text-fg-muted hover:opacity-70",e&&"disabled:text-fg"],r==="outline"&&["bg-transparent hover:bg-transparent focus:bg-transparent disabled:bg-transparent","border-border hover:border-border-emphasis focus:border-border-emphasis disabled:border-border-subtle","text-fg disabled:text-fg-muted",e&&["disabled:border-border","disabled:text-fg"]],r==="solid"&&["bg-surface-inverse-hover hover:bg-surface-inverse focus:bg-surface-inverse disabled:bg-surface-inverse","border-surface-inverse-hover hover:border-fg focus:border-fg disabled:border-border-subtle","text-white disabled:text-fg-subtle",e&&["disabled:border-fg","disabled:text-white"]]];return f(c,{ref:b,type:"button",isDisabled:n,className:i=>{const l=typeof s=="function"?s(i):s;return g(u,l)},...a})});export{w as Button,w as default,h as resolveVariant};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsxs as
|
|
1
|
+
"use client";import{jsxs as l,Fragment as h,jsx as e}from"react/jsx-runtime";import{useMemo as b,Suspense as x,useState as v}from"react";import{Accordion as g,AccordionItem as w}from"../../Accordion/Accordion.js";import{Button as f}from"../../Button/Button.js";import"../../../utils/cn.js";import{ModalOverlay as y}from"../../ModalOverlay/ModalOverlay.js";import{Modal as N}from"../../Modal/Modal.js";import{Drawer as k}from"../../Drawer/Drawer.js";import{Cross as C}from"../../Icon/Cross.js";import{CrossSmall as A}from"../../Icon/CrossSmall.js";import{Search as D}from"../../Icon/Search.js";import{FilterRenderer as I}from"../FilterRenderer/FilterRenderer.js";import{mergeFilters as S}from"../../../utils/collection/mergeFilters.js";import{FilterAccordionTitle as j}from"../FilterAccordionTitle/FilterAccordionTitle.js";import{EmptyResults as L}from"../EmptyResults/EmptyResults.js";import"react-aria-components";import"../../Icon/DownArrow.js";import"framer-motion";import"../../../utils/env.js";import"../../../utils/swatch.js";import"../FilterRenderer/Renderer/Option.js";import"../../Checkbox/Checkbox.js";import"../FilterRenderer/Renderer/useOptionFilterToggle.js";import"../FilterRenderer/Renderer/Swatch.js";import"../FilterRenderer/Renderer/Swatch/SwatchColor.js";import"../../Icon/CheckIcon.js";import"../../../utils/color.js";import"../FilterRenderer/Renderer/Swatch/SwatchImage.js";import"../FilterRenderer/Renderer/Swatch/SwatchText.js";import"../FilterRenderer/Renderer/Boolean.js";import"../../RadioGroup/RadioGroup.js";import"../../Radio/Radio.js";import"../FilterRenderer/Renderer/Range.js";function M(){return e("svg",{width:"16",height:"16",viewBox:"0 0 16 16",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:e("path",{d:"M4 6L8 10L12 6",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round"})})}function V({value:a,onChange:r}){const[o,c]=v(a),m=()=>{r(o.trim())};return l("div",{className:"relative",children:[e("input",{type:"search",value:o,onChange:i=>c(i.target.value),onKeyDown:i=>{i.key==="Enter"&&(i.preventDefault(),m())},onBlur:m,placeholder:"Search products...","aria-label":"Search products",autoComplete:"off",className:"w-full h-9 pl-8 pr-8 rounded-md border border-border bg-white text-sm text-fg placeholder:text-fg-subtle focus:outline-none focus:border-border-emphasis [&::-webkit-search-cancel-button]:hidden"}),o&&e("button",{type:"button",onClick:()=>{c(""),r("")},"aria-label":"Clear search",className:"absolute right-2 top-1/2 -translate-y-1/2 p-0.5 rounded-full hover:bg-surface-subtle transition-colors cursor-pointer",children:e(A,{})}),e("span",{className:"absolute left-2.5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 [&>svg]:w-full [&>svg]:h-full text-fg-subtle",children:e(D,{})})]})}function ue({availableFilters:a,appliedFilters:r,onFilterChange:o,showMobileFilters:c,setShowMobileFilters:m,searchQuery:s,onSearchChange:n,totalItems:i}){const u=b(()=>S(r,a).filter(t=>t.includeInFilters!==!1),[r,a]),d=n&&e(V,{value:s??"",onChange:n},s),p=()=>u.length>0&&e(g,{selectionMode:"multiple",defaultSelectedKeys:"all",className:"pt-2 mt-3",showDivider:!1,itemClasses:{base:"py-1",title:"text-sm font-medium leading-7",trigger:"py-2 border-b border-gray-200",indicator:'data-[open="true"]:rotate-180',content:"pt-2 pb-3"},children:u.map(t=>e(w,{textValue:t.label,title:e(j,{filter:t,appliedFilters:r,onFilterChange:o}),disableIndicatorAnimation:!0,indicator:M,children:e("div",{className:"pt-2",children:e(I,{filter:t,onFilterChange:o,appliedFilters:r,variant:"sidebar"})})},t.code))});return l(h,{children:[l("div",{className:"hidden lg:flex lg:flex-col lg:basis-[16.5rem] lg:shrink-0",children:[d,p(),r.length>0&&e("button",{type:"button",className:"text-sm text-fg-muted hover:text-slate-700 transition-colors mt-4 mb-8 cursor-pointer",onClick:()=>o([]),children:"Clear all filters"})]}),e("div",{className:"lg:hidden",children:e(x,{children:e(y,{isOpen:c,onOpenChange:t=>m(t),isDismissable:!0,children:e(N,{children:e(k,{size:"30rem",direction:"left","aria-label":"Filters",className:"p-5",children:({close:t})=>l("div",{className:"flex flex-col w-full h-full overflow-hidden",children:[l("div",{className:"flex items-center w-full justify-between",children:[e("span",{className:"text-xl font-bold text-slate-800",children:"Filters"}),e(f,{variant:"ghost","aria-label":"Close",onPress:()=>t(),className:"justify-start size-auto p-0",children:e(C,{})})]}),l("div",{className:"flex flex-col flex-1 overflow-x-hidden overflow-y-auto [scrollbar-width:none] [-webkit-overflow-scrolling:touch] [&::-webkit-scrollbar]:hidden",children:[d&&e("div",{className:"mt-3",children:d}),p(),i===0&&e(L,{searchQuery:s,hasFilters:r.length>0,onClearFilters:()=>o([]),onClearSearch:s&&n?()=>n(""):void 0})]}),l("div",{className:"shrink-0 pt-3 flex flex-col gap-3 border-t border-gray-200",children:[r.length>0&&e("button",{type:"button",className:"text-sm py-2 text-fg-muted hover:text-slate-700 transition-colors cursor-pointer",onClick:()=>o([]),children:"Clear all"}),e(f,{className:"w-full py-6 text-base rounded-xl",variant:"primary",onPress:()=>t(),children:"View Results"})]})]})})})})})})]})}export{ue as CollectionFilters};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsxs as
|
|
1
|
+
"use client";import{jsxs as l,jsx as t}from"react/jsx-runtime";function r({searchQuery:e,hasFilters:n,onClearFilters:s,onClearSearch:o}){const i=n&&s||e&&o;return l("div",{className:"flex flex-col items-center justify-center py-16 px-4 text-center col-span-full",children:[l("svg",{xmlns:"http://www.w3.org/2000/svg",width:"48",height:"48",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"1.5",strokeLinecap:"round",strokeLinejoin:"round",className:"text-slate-300 mb-4",children:[t("circle",{cx:"11",cy:"11",r:"8"}),t("path",{d:"m21 21-4.3-4.3"})]}),t("h3",{className:"text-lg font-semibold text-slate-800 mb-2",children:"No results found"}),e&&l("p",{className:"text-sm text-fg-muted mb-4",children:["We couldn't find anything matching “",e,"”"]}),!e&&!n&&t("p",{className:"text-sm text-fg-muted mb-4",children:"No products match your current selection."}),i&&l("div",{className:"flex items-center gap-3",children:[e&&o&&t("button",{type:"button",onClick:o,className:"text-sm font-medium text-slate-700 hover:text-fg underline underline-offset-2 transition-colors cursor-pointer",children:"Clear search"}),n&&s&&e&&o&&t("span",{className:"text-slate-300",children:"|"}),n&&s&&t("button",{type:"button",onClick:s,className:"text-sm font-medium text-slate-700 hover:text-fg underline underline-offset-2 transition-colors cursor-pointer",children:"Clear all filters"})]}),!i&&t("p",{className:"text-xs text-fg-subtle",children:"Try a different search term or browse our categories."})]})}export{r as EmptyResults};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as e}from"react/jsx-runtime";import{forwardRef as
|
|
1
|
+
"use client";import{jsx as e}from"react/jsx-runtime";import{forwardRef as s}from"react";import{Dialog as f}from"react-aria-components";import{motion as d}from"framer-motion";import{cn as u}from"../../utils/cn.js";const m=s(({style:o,animationVariants:a,...t},i)=>{const r=typeof o=="function"?o(t):o;return e(f,{...t,ref:i,style:r})});let n=null;function c(){return typeof window>"u"?null:(n||(n=d.create(m)),n)}const l=c();function y({className:o,children:a,animationVariants:t,...i}){const r={className:u("p-8 outline-0 max-w-max w-screen absolute top-2/4 left-2/4","-translate-x-1/2 -translate-y-1/2","max-h-[calc(100dvh-2rem)] overflow-y-auto","shadow-[0_8px_24px_rgba(0,0,0,0.1)] rounded-lg bg-white border border-solid border-border-emphasis",o),...i};return l?e(l,{...r,variants:t||{hidden:{opacity:0},visible:{opacity:1}},children:a}):e(m,{...r,children:a})}export{y as Dialog,y as default};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { DrawerProps } from './Drawer.types';
|
|
2
|
-
export declare function Drawer({ direction, children, className, animationVariants, ...props }: DrawerProps): import("react/jsx-runtime").JSX.Element;
|
|
2
|
+
export declare function Drawer({ direction, children, className, animationVariants, fullScreen, ...props }: DrawerProps): import("react/jsx-runtime").JSX.Element;
|
|
3
3
|
export default Drawer;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsx as n}from"react/jsx-runtime";import{forwardRef as
|
|
1
|
+
"use client";import{jsx as n}from"react/jsx-runtime";import{forwardRef as m}from"react";import{Dialog as b}from"react-aria-components";import{m as d}from"framer-motion";import{cn as h}from"../../utils/cn.js";const p={top:{hidden:{y:"-100%",transition:{ease:"easeIn"}},visible:{y:0,transition:{ease:"easeOut"}}},bottom:{hidden:{y:"100%",transition:{ease:"easeIn"}},visible:{y:0,transition:{ease:"easeOut"}}},right:{hidden:{x:"100%",transition:{ease:"easeIn"}},visible:{x:0,transition:{ease:"easeOut"}}},left:{hidden:{x:"-100%",transition:{ease:"easeIn"}},visible:{x:0,transition:{ease:"easeOut"}}}},u=m(({style:t,size:e="20rem",...o},a)=>{const r=typeof t=="function"?t(o):{...t,"--drawer-size":`${e}`};return n(b,{...o,ref:a,style:r})});let i=null;function w(){return typeof window>"u"?null:(i||(i=d.create(u)),i)}const l=w();function y({direction:t="right",children:e,className:o,animationVariants:a,fullScreen:r=!1,...f}){const s={className:h("fixed p-8 bg-white","outline-none focus:outline-none focus-visible:outline-none","border-solid border-border-subtle",(t==="top"||t==="bottom")&&(r?"max-h-[var(--visual-viewport-height)]":"max-h-[min(var(--visual-viewport-height),88vh)]"),(t==="left"||t==="right")&&(r?"max-w-full":"max-w-[88vw]"),t==="top"&&"top-0 left-0 right-0 bottom-auto border-b shadow-[0_8px_24px_rgba(0,0,0,0.1)] h-[var(--drawer-size)]",t==="right"&&"top-0 left-auto bottom-0 right-0 border-l shadow-[-8px_0_24px_rgba(0,0,0,0.1)] w-[var(--drawer-size)]",t==="bottom"&&"bottom-0 left-0 right-0 top-auto border-t shadow-[0_-8px_24px_rgba(0,0,0,0.1)] h-[var(--drawer-size)]",t==="left"&&"top-0 bottom-0 left-0 right-auto border-r shadow-[8px_0_24px_rgba(0,0,0,0.1)] w-[var(--drawer-size)]",o),"data-direction":t,...f};return l?n(l,{variants:a||p[t],...s,children:e}):n(u,{...s,children:e})}export{y as Drawer,y as default};
|
|
@@ -7,4 +7,11 @@ export type DrawerProps = Omit<DialogProps, 'onAnimationStart'> & {
|
|
|
7
7
|
style?: MotionStyle;
|
|
8
8
|
animationVariants?: Variants;
|
|
9
9
|
size?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Opts out of the viewport clamp (CTAP-1877) so `size` can legitimately fill
|
|
12
|
+
* the screen. Without this, a `size` wider/taller than the viewport is capped
|
|
13
|
+
* at 88vw/88vh so a sliver of the page stays visible and tappable — full
|
|
14
|
+
* screen must be chosen with this prop, not arrived at by an oversized size.
|
|
15
|
+
*/
|
|
16
|
+
fullScreen?: boolean;
|
|
10
17
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsxs as
|
|
1
|
+
"use client";import{jsxs as b,jsx as t}from"react/jsx-runtime";import{cn as s}from"../../utils/cn.js";function x({cooldownSeconds:e,isResending:n,canResend:a,onResend:i,prompt:r="Didn't get a code?",label:d="Resend code",resendingLabel:l="Sending…",availableAnnouncement:o="You can request a new code now.",className:c,buttonClassName:u}){const p=e>0?`Resend in ${e}s`:n?l:d;return b("p",{className:s("text-sm",c),children:[r," ",t("button",{type:"button",onClick:i,disabled:!a,className:s("underline cursor-pointer disabled:no-underline disabled:opacity-50 disabled:cursor-not-allowed",u),children:p}),t("span",{"aria-live":"polite",className:"sr-only",children:e===0&&!n?o:""})]})}export{x as ResendCode,x as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use client";import{jsxs as G,jsx as c}from"react/jsx-runtime";import{forwardRef as Se,useRef as g,useState as
|
|
1
|
+
"use client";import{jsxs as G,jsx as c}from"react/jsx-runtime";import{forwardRef as Se,useRef as g,useState as we,useMemo as I,isValidElement as Ke,useEffect as R}from"react";import{useFilter as Ve,useObjectRef as Be,useComboBox as Fe,useButton as Ne,HiddenSelect as Pe}from"react-aria";import{Section as Me,useComboBoxState as Ae}from"react-stately";import{cn as r}from"../../utils/cn.js";import{Pulse as Oe}from"../Loader/Pulse.js";import{SelectTags as Te,ChevronIcon as ke,SelectOverlay as De,SelectListBox as Le}from"./parts.js";import"react-aria-components";function S(s){return s?typeof s.rendered=="string"?s.rendered:s.textValue??"":""}function Ee(s,i){const f=i.trim().toLowerCase();let p=null,a=s.getFirstKey();for(;a!=null;){const m=s.getItem(a);if(m?.type==="item"&&(p==null&&(p=a),f.length>0&&(m.textValue??"").toLowerCase().split(/\s+/).includes(f)))return a;a=s.getKeyAfter(a)}return p}function _(s,i){const f=s.toLowerCase(),p=f.split(/\s+/);return p.includes(i)?0:f.startsWith(i)?1:p.some(a=>a.startsWith(i))?2:3}function J(s){const i=s.props;return typeof i.textValue=="string"?i.textValue:typeof i.children=="string"?i.children:""}function He(s,i){const{label:f,description:p,errorMessage:a,children:m,placeholder:k,isLoading:D,slots:L,popoverOffset:Q,className:U,classNames:l,autoComplete:w,menuTrigger:X,selectTextOnFocus:Y=!0,autoFocusFirstOption:Z=!0,searchable:je,selectionMode:$="single",selectedKey:ee,defaultSelectedKey:te,selectedKeys:K,defaultSelectedKeys:V,onSelectionChange:E,name:ne,...x}=s,u=$==="multiple",B=!!x.isDisabled,C=g(!1),h=g(null),d=x.inputValue!==void 0,[F,y]=we(""),N=d?x.inputValue:F,oe=I(()=>{const e=(N??"").trim().toLowerCase();if(d||typeof m=="function"||e===""||!Array.isArray(m))return m;const n=m,o=n.filter(b=>Ke(b));return o.length!==n.length||o.length===0||o.some(b=>b.type===Me)?m:[...o].sort((b,Re)=>_(J(b),e)-_(J(Re),e))},[m,d,N]),re=I(()=>u&&K?[...K]:void 0,[u,K]),se=I(()=>u&&V?[...V]:void 0,[u,V]),H={...x,label:f,children:oe,placeholder:k,isDisabled:B,menuTrigger:X??"focus",autoComplete:w,inputValue:N,...u?{selectionMode:"multiple",value:re,defaultValue:se,onChange:E}:{selectedKey:ee,defaultSelectedKey:te,onSelectionChange:e=>{if(!d){const n=e!=null?h.current?.collection.getItem(e):null;y(S(n))}E?.(e)}},onInputChange:e=>{d||y(e),x.onInputChange?.(e),!u&&e===""&&h.current?.selectedKey!=null&&h.current.setSelectedKey(null)}},{contains:le}=Ve({sensitivity:"base"}),t=Ae({...H,defaultFilter:le,allowsEmptyCollection:!0});R(()=>{h.current=t});const j=g(0),P=t.selectionManager.selectedKeys.size;R(()=>{u&&(P>j.current&&!d&&y(""),j.current=P)},[u,P,d]);const M=S(t.selectedItem);R(()=>{d||u||C.current||F===M||y(M)},[M,d,F,u]);const ie=g(null),W=Be(i),q=g(null),z=g(null),A=g(null),O=g(null),T=g(null),{inputProps:ue,listBoxProps:ce,labelProps:ae,buttonProps:de,descriptionProps:fe,errorMessageProps:pe}=Fe({...H,inputRef:W,listBoxRef:q,popoverRef:z,buttonRef:A},t),{buttonProps:me}=Ne(de,A),v=t.displayValidation.isInvalid,{onKeyDown:ge,onFocus:be,onBlur:he,...xe}=ue,ye=e=>{e.key==="Home"||e.key==="End"||ge?.(e)};R(()=>{const e=t.selectionManager;if(!Z||!t.isOpen||t.inputValue.length===0){T.current=null;return}const n=e.focusedKey;if(n!=null&&n!==T.current)return;const o=Ee(t.collection,t.inputValue);T.current=o??null,o!=null&&o!==n&&e.setFocusedKey(o)});const Ce=e=>{be?.(e);const n=C.current;if(C.current=!0,n||!Y)return;const o=e.target,b=o.value;b&&requestAnimationFrame(()=>{o.value===b&&o.select()})},ve=e=>{he?.(e);const n=e.currentTarget;requestAnimationFrame(()=>{document.activeElement!==n&&(C.current=!1,h.current?.close(),d||y(u?"":S(h.current?.selectedItem)))})},Ie=I(()=>new Proxy(t,{get(e,n){return n==="setValue"?o=>e.setSelectedKey(o):Reflect.get(e,n)}}),[t]);return G("div",{ref:ie,className:r("flex flex-col","w-full",U),children:[c("label",{...ae,className:r("flex","text-xs text-fg-muted",l?.label),children:f}),c(Pe,{state:Ie,triggerRef:O,label:f,name:ne,isDisabled:B,autoComplete:w}),u&&c(Te,{items:(t.selectedItems??[]).map(e=>({id:e.key,label:S(e)})),onRemove:e=>t.selectionManager.toggleSelection(e),classNames:{tagGroup:r(l?.tagGroup),tag:r(l?.tag),tagRemoveButton:r(l?.tagRemoveButton)}}),G("div",{ref:O,className:r("flex","relative","w-full",l?.comboBoxContainer),children:[c("input",{...xe,onKeyDown:ye,onFocus:Ce,onBlur:ve,ref:W,placeholder:k,autoComplete:w,className:r("border border-solid border-border","text-sm text-fg","py-0 px-2","h-10 w-full","m-0","rounded-md","bg-white","transition-all ease-in-out duration-200","hover:border-border-emphasis","focus:border-border-emphasis focus:outline focus:outline-2 focus:outline-border-subtle","disabled:bg-surface-subtle disabled:border-border",D?"disabled:text-fg":"disabled:text-fg-muted",v&&"border-danger-soft bg-danger-surface text-danger placeholder:text-fg-subtle",v&&"hover:border-danger focus:border-danger focus:outline-danger-ring",l?.input)}),D?c("div",{className:r("absolute top-2 right-2","block",l?.loader),children:L?.loadingIcon||c(Oe,{})}):c("button",{...me,ref:A,className:r("absolute top-2 right-0","block","border-none bg-none","cursor-pointer disabled:cursor-not-allowed",l?.arrowButton),children:c(ke,{isOpen:t.isOpen,isDisabled:B,isInvalid:v,buttonIcon:L?.buttonIcon})}),t.isOpen&&t.collection.size>0&&c(De,{state:t,triggerRef:O,popoverRef:z,offset:Q,className:r(l?.listContainer),children:c(Le,{state:t,listBoxProps:ce,listBoxRef:q,optionVirtualFocus:!0,optionClassName:r(l?.option),sectionHeadingClassName:r(l?.sectionHeading),className:r("max-h-80","overflow-y-auto","outline-none",l?.list)})})]}),p&&c("span",{...fe,className:r("flex","text-xs","text-fg-muted",l?.description),children:p}),v&&typeof a=="string"&&c("span",{...pe,className:r("flex","text-xs","text-danger-soft",l?.errorMessage),children:a})]})}const Ye=Se(He);export{Ye as ComboBoxSelect,Ye as default};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# BUGFIX: a `Dialog` taller than the viewport scrolls inside itself
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-09-19
|
|
4
|
+
**Package:** `@clicktap/ui`
|
|
5
|
+
**Affects:** `src/components/Dialog/Dialog.tsx` — every `<Dialog>` rendered inside `<Modal>`.
|
|
6
|
+
**Classification:** BUGFIX
|
|
7
|
+
**Breaking:** No API change. **Visual:** a dialog whose content is taller than the viewport is now capped at the viewport height minus 2rem and scrolls its own content; shorter dialogs render exactly as before.
|
|
8
|
+
**Ticket:** CTAP-1791 (epic CTAP-1785)
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
`Dialog` is centered with `absolute top-2/4 left-2/4 -translate-x-1/2 -translate-y-1/2` and had
|
|
13
|
+
no height limit. A dialog taller than the viewport therefore grew past both edges: its top half
|
|
14
|
+
sat above the screen and its footer, the Cancel and Update buttons, below it, with no way to
|
|
15
|
+
scroll to them because the overlay itself does not scroll.
|
|
16
|
+
|
|
17
|
+
CTAP-1791 put the billing address fields into the saved-card edit form, so "Update payment
|
|
18
|
+
method" now carries cardholder name, expiry, postal code, ten address fields and the default
|
|
19
|
+
control. On a laptop viewport the buttons were off screen.
|
|
20
|
+
|
|
21
|
+
## What changed
|
|
22
|
+
|
|
23
|
+
The dialog gets `max-h-[calc(100dvh-2rem)] overflow-y-auto`. The centering is untouched, so a
|
|
24
|
+
capped dialog still sits in the middle with 1rem clear of each edge and its content scrolls
|
|
25
|
+
inside the rounded, bordered box. `dvh` follows the mobile browser chrome as it shows and hides.
|
|
26
|
+
|
|
27
|
+
## Consumer action
|
|
28
|
+
|
|
29
|
+
None. A consumer that already wrapped its dialog content in its own scroll container keeps
|
|
30
|
+
working; the outer cap only engages when the content is taller than the viewport. A consumer that
|
|
31
|
+
passes `className` with its own `max-h-*` or `overflow-*` still wins, since `className` merges
|
|
32
|
+
last.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# BUGFIX: `Drawer` no longer silently becomes a full-screen takeover
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-09-23
|
|
4
|
+
**Package:** `@clicktap/ui`
|
|
5
|
+
**Affects:** `src/components/Drawer/Drawer.tsx`, `Drawer.types.ts` — every `<Drawer>` whose `size` (or default) exceeds the viewport along its opening axis.
|
|
6
|
+
**Classification:** BUGFIX
|
|
7
|
+
**Breaking:** No API change. **Visual:** a `left`/`right` drawer wider than 88% of the viewport, or a `top`/`bottom` drawer taller than 88% of it, now leaves a visible sliver of the page instead of covering it entirely. A drawer that already fit is unchanged.
|
|
8
|
+
**Ticket:** CTAP-1877
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
`Drawer` rendered its `size` as a custom property and capped it with `max-w-full` (left/right) or
|
|
13
|
+
`max-h-[var(--visual-viewport-height)]` (top/bottom). Both caps are exactly 100% of the viewport, so
|
|
14
|
+
a `size` wider/taller than the viewport did not render "nearly full" — it rendered **exactly** full:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
w-[var(--drawer-size)] max-w-full → effective width = 100vw
|
|
18
|
+
h-[var(--drawer-size)] max-h-[var(--visual-viewport-height)] → effective height = 100%
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`size="30rem"` (480px) on a 375px phone hit this on every storefront drawer that used it (header
|
|
22
|
+
menu, cart, add-items, collection filters). The **default** `size` (`20rem` = 320px) hit it too,
|
|
23
|
+
leaving only 55px at 375px and nothing at all at 320px.
|
|
24
|
+
|
|
25
|
+
A drawer with no visible edge reads as a new page rather than a layer over the current one, and it
|
|
26
|
+
removes the obvious place to tap to dismiss.
|
|
27
|
+
|
|
28
|
+
## What changed
|
|
29
|
+
|
|
30
|
+
The cap is now 88% of the viewport along the drawer's opening axis, so the declared `size` is a
|
|
31
|
+
maximum rather than a promise:
|
|
32
|
+
|
|
33
|
+
- `left` / `right`: `max-w-[88vw]` (was `max-w-full`)
|
|
34
|
+
- `top` / `bottom`: `max-h-[min(var(--visual-viewport-height),88vh)]` (was `max-h-[var(--visual-viewport-height)]`)
|
|
35
|
+
|
|
36
|
+
88vw/88vh leaves a 45–50px sliver at common phone widths and changes nothing once the drawer's own
|
|
37
|
+
size is already under 88% of the viewport — which every desktop width is, for every `size` this
|
|
38
|
+
package has shipped.
|
|
39
|
+
|
|
40
|
+
**New `fullScreen` prop** is the escape hatch: `<Drawer fullScreen>` restores the old unclamped
|
|
41
|
+
behavior (`max-w-full` / `max-h-[var(--visual-viewport-height)]`) for the rare drawer that
|
|
42
|
+
legitimately wants the whole screen. Full-screen is now something you ask for, not something an
|
|
43
|
+
oversized `size` gives you by accident.
|
|
44
|
+
|
|
45
|
+
## Consumer Impact
|
|
46
|
+
|
|
47
|
+
**No action needed for a drawer whose `size` already fit its viewport.** Nothing changes for it.
|
|
48
|
+
|
|
49
|
+
**Drop any hand-rolled clamp.** If you were passing `size="min(30rem, 88vw)"` (or similar) as a
|
|
50
|
+
workaround, remove it — `size="30rem"` now behaves the same way natively:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
// BEFORE
|
|
54
|
+
<Drawer size="min(30rem, 88vw)" direction="left" aria-label="Filters">
|
|
55
|
+
|
|
56
|
+
// AFTER
|
|
57
|
+
<Drawer size="30rem" direction="left" aria-label="Filters">
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**If a drawer genuinely needs the full screen**, add `fullScreen`:
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
<Drawer size="100vw" direction="right" fullScreen aria-label="Full-screen picker">
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Without `fullScreen`, a `size="100vw"` (or any value ≥ 88% of the viewport) is now clamped to 88vw —
|
|
67
|
+
that clamp is the point of this fix, so an existing call site that relied on `size` alone to reach
|
|
68
|
+
full screen needs the new prop.
|
|
69
|
+
|
|
70
|
+
The overlay's own dismiss-on-tap-outside behavior (`isDismissable` on `ModalOverlay`) is unaffected
|
|
71
|
+
by this change; the sliver only makes that existing behavior discoverable.
|
|
72
|
+
|
|
73
|
+
## Verification
|
|
74
|
+
|
|
75
|
+
`Drawer.spec.tsx` — 10 tests: for each of the four directions, asserts the clamp class is present
|
|
76
|
+
and the old unconditional full-viewport class is absent, plus the `fullScreen` escape hatch restores
|
|
77
|
+
the old classes and removes the clamp. Also covers that the default `size` (`20rem`) is still subject
|
|
78
|
+
to the clamp, and that a `size` under the clamp still renders via `w-[var(--drawer-size)]` /
|
|
79
|
+
`h-[var(--drawer-size)]` (i.e. the fix does not force every drawer to exactly 88vw/88vh — it is a
|
|
80
|
+
ceiling, not a fixed size).
|
|
81
|
+
|
|
82
|
+
These are cancellation assertions, the same shape as `Button`'s `variant="link"` tests: they check
|
|
83
|
+
the old full-viewport class is **gone**, not merely that a clamp class was appended alongside it —
|
|
84
|
+
`tailwind-merge` collapses conflicting `max-w-*`/`max-h-*` utilities to the last one it sees, so a
|
|
85
|
+
fix that left both classes present would still resolve to whichever one happened to come last.
|
|
86
|
+
|
|
87
|
+
⚠️ **Not visually verified against real touch devices.** The tests assert which Tailwind classes
|
|
88
|
+
survive in jsdom; they cannot show that 88vw/88vh is the right percentage for a given storefront's
|
|
89
|
+
breakpoints, only that some percentage other than 100% is now applied. The ticket already flags this
|
|
90
|
+
as a design judgment call, not an engineering one.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# BUGFIX: interactive elements show a pointer cursor under Tailwind v4
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-09-23
|
|
4
|
+
**Package:** `@clicktap/ui`
|
|
5
|
+
**Affects:** `src/components/Button/Button.tsx`, `ResendCode/ResendCode.tsx`, `Select/ComboBoxSelect.tsx`, `Collection/EmptyResults/EmptyResults.tsx`, `Collection/CollectionFilters/CollectionFilters.tsx`.
|
|
6
|
+
**Classification:** BUGFIX
|
|
7
|
+
**Breaking:** No API change. **Visual:** hovering an enabled button in any of the affected components now shows a pointer cursor instead of the default arrow. Disabled states are unchanged (still `not-allowed` where they had it, still no explicit cursor where they didn't).
|
|
8
|
+
**Ticket:** CTAP-1874
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Tailwind v4's Preflight changed the `<button>` reset from `cursor: pointer` to `cursor: default`, so
|
|
13
|
+
a class that used to be unnecessary now is. `Button` and `ResendCode` never set `cursor-pointer`
|
|
14
|
+
explicitly — they relied on the browser/Tailwind v3 default — so under Tailwind v4 they drew the
|
|
15
|
+
arrow cursor on hover instead of a hand. `Input`'s password-reveal toggle already set
|
|
16
|
+
`cursor-pointer` explicitly, which is what made this an inconsistency rather than a design decision:
|
|
17
|
+
the library already knew to do this, just not everywhere.
|
|
18
|
+
|
|
19
|
+
The ticket asked for a grep of the rest of the package for `<button` without a cursor class rather
|
|
20
|
+
than stopping at the two reported components. That turned up three more raw `<button>` elements
|
|
21
|
+
missing it: the combobox's dropdown-arrow toggle (`ComboBoxSelect`), and the "Clear search" / "Clear
|
|
22
|
+
all filters" text buttons in `EmptyResults` and `CollectionFilters` (the last of these appears at
|
|
23
|
+
three call sites: the search-clear icon, the desktop sidebar, and the mobile filter drawer).
|
|
24
|
+
`Pagination`, `ButtonSelect` and `MultiButtonSelect` already had `cursor-pointer` and needed no
|
|
25
|
+
change.
|
|
26
|
+
|
|
27
|
+
## What changed
|
|
28
|
+
|
|
29
|
+
Added `cursor-pointer` to the enabled state of every affected button, alongside `disabled:cursor-not-allowed`
|
|
30
|
+
where a disabled state exists:
|
|
31
|
+
|
|
32
|
+
| Component | Before | After |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `Button` | no cursor class | `cursor-pointer disabled:cursor-not-allowed` |
|
|
35
|
+
| `ResendCode` | `disabled:cursor-not-allowed` only | `cursor-pointer disabled:cursor-not-allowed` |
|
|
36
|
+
| `ComboBoxSelect` (arrow toggle) | no cursor class | `cursor-pointer disabled:cursor-not-allowed` |
|
|
37
|
+
| `EmptyResults` (both actions) | no cursor class | `cursor-pointer` (no disabled state on these buttons) |
|
|
38
|
+
| `CollectionFilters` (all three call sites) | no cursor class | `cursor-pointer` (no disabled state on these buttons) |
|
|
39
|
+
|
|
40
|
+
`Button` gaining `disabled:cursor-not-allowed` is new — it had no cursor handling in either state
|
|
41
|
+
before this fix — and matches the pattern already established by `ResendCode` and `Input`.
|
|
42
|
+
|
|
43
|
+
## Consumer Impact
|
|
44
|
+
|
|
45
|
+
**No action needed to upgrade.** This only adds a `cursor-pointer` (and, for `Button`, a
|
|
46
|
+
`disabled:cursor-not-allowed`) utility to the base class list; nothing is removed, and `className`
|
|
47
|
+
already merges last via `cn`/tailwind-merge, so a consumer overriding cursor at the call site
|
|
48
|
+
continues to win.
|
|
49
|
+
|
|
50
|
+
**Drop any hand-rolled workaround.** If you were passing a `className`/`buttonClassName` purely to
|
|
51
|
+
restore the pointer cursor under Tailwind v4, remove it:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
// BEFORE
|
|
55
|
+
<ResendCode buttonClassName="cursor-pointer underline" ... />
|
|
56
|
+
|
|
57
|
+
// AFTER
|
|
58
|
+
<ResendCode ... />
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Verification
|
|
62
|
+
|
|
63
|
+
New/updated specs, run via `nx run frontend-module-ui:test`:
|
|
64
|
+
|
|
65
|
+
- `Button.spec.tsx` — `cursor-pointer` asserted for all four `variant` aliases (`primary`,
|
|
66
|
+
`secondary`, `tertiary`, `link`).
|
|
67
|
+
- `ResendCode.test.tsx` — `cursor-pointer` asserted while enabled; `disabled:cursor-not-allowed`
|
|
68
|
+
still asserted while disabled.
|
|
69
|
+
- `Select/ComboBoxSelect.spec.tsx` (new) — `cursor-pointer` on the dropdown-arrow toggle, rendered
|
|
70
|
+
through the public `<Select searchable>`.
|
|
71
|
+
- `Collection/EmptyResults/EmptyResults.spec.tsx` (new) — `cursor-pointer` on both "Clear search" and
|
|
72
|
+
"Clear all filters".
|
|
73
|
+
- `Collection/CollectionFilters/CollectionFilters.spec.tsx` (new) — `cursor-pointer` on the
|
|
74
|
+
search-clear icon button and the desktop "Clear all filters" button.
|
|
75
|
+
|
|
76
|
+
All ten new/changed assertions were confirmed to fail against the pre-fix code (missing
|
|
77
|
+
`cursor-pointer` in the rendered className) before the fix landed, then pass after.
|
|
78
|
+
|
|
79
|
+
⚠️ **Not independently re-verified against a live Tailwind v4 build.** The tests assert the utility
|
|
80
|
+
class is present in the rendered `className`; they cannot show the class actually resolves to a
|
|
81
|
+
hand cursor in a given consumer's compiled CSS (e.g. if a consumer's Tailwind config purges
|
|
82
|
+
`cursor-pointer` for some reason). The mechanism reported in the ticket — Preflight's `cursor:
|
|
83
|
+
default` reset on `<button>` under Tailwind v4 — was not independently reproduced in a browser as
|
|
84
|
+
part of this fix; it was taken from the ticket's own evidence.
|
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"path": "2026/09/ui-2026-09-23-interactive-elements-cursor-pointer.md",
|
|
4
|
+
"module": "libs/frontend-modules/ui",
|
|
5
|
+
"package": "@clicktap/ui",
|
|
6
|
+
"date": "2026-09-23",
|
|
7
|
+
"title": "BUGFIX: interactive elements show a pointer cursor under Tailwind v4",
|
|
8
|
+
"classification": "BUGFIX",
|
|
9
|
+
"breaking": "No API change. **Visual:** hovering an enabled button in any of the affected components now shows a pointer cursor instead of the default arrow. Disabled states are unchanged (still `not-allowed` where they had it, still no explicit cursor where they didn't).",
|
|
10
|
+
"affects": "`src/components/Button/Button.tsx`, `ResendCode/ResendCode.tsx`, `Select/ComboBoxSelect.tsx`, `Collection/EmptyResults/EmptyResults.tsx`, `Collection/CollectionFilters/CollectionFilters.tsx`."
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"path": "2026/09/ui-2026-09-23-drawer-viewport-clamp.md",
|
|
14
|
+
"module": "libs/frontend-modules/ui",
|
|
15
|
+
"package": "@clicktap/ui",
|
|
16
|
+
"date": "2026-09-23",
|
|
17
|
+
"title": "BUGFIX: `Drawer` no longer silently becomes a full-screen takeover",
|
|
18
|
+
"classification": "BUGFIX",
|
|
19
|
+
"breaking": "No API change. **Visual:** a `left`/`right` drawer wider than 88% of the viewport, or a `top`/`bottom` drawer taller than 88% of it, now leaves a visible sliver of the page instead of covering it entirely. A drawer that already fit is unchanged.",
|
|
20
|
+
"affects": "`src/components/Drawer/Drawer.tsx`, `Drawer.types.ts` — every `<Drawer>` whose `size` (or default) exceeds the viewport along its opening axis."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": "2026/09/ui-2026-09-19-dialog-scrolls-within-viewport.md",
|
|
24
|
+
"module": "libs/frontend-modules/ui",
|
|
25
|
+
"package": "@clicktap/ui",
|
|
26
|
+
"date": "2026-09-19",
|
|
27
|
+
"title": "BUGFIX: a `Dialog` taller than the viewport scrolls inside itself",
|
|
28
|
+
"classification": "BUGFIX",
|
|
29
|
+
"breaking": "No API change. **Visual:** a dialog whose content is taller than the viewport is now capped at the viewport height minus 2rem and scrolls its own content; shorter dialogs render exactly as before.",
|
|
30
|
+
"affects": "`src/components/Dialog/Dialog.tsx` — every `<Dialog>` rendered inside `<Modal>`."
|
|
31
|
+
},
|
|
2
32
|
{
|
|
3
33
|
"path": "2026/09/ui-2026-09-18-pin-input-autofill-and-controlled-value.md",
|
|
4
34
|
"module": "libs/frontend-modules/ui",
|