@clicktap/ui 2.0.10 → 2.1.0

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,5 +1,5 @@
1
1
  import { ButtonVariant } from './Button.types';
2
- export declare function resolveVariant(variant: ButtonVariant): 'solid' | 'outline' | 'ghost';
2
+ export declare function resolveVariant(variant: ButtonVariant): 'solid' | 'outline' | 'ghost' | 'link';
3
3
  export declare const Button: import('react').ForwardRefExoticComponent<import('react-aria-components').ButtonProps & {
4
4
  isLoading?: boolean;
5
5
  variant?: ButtonVariant;
@@ -1 +1 @@
1
- "use client";import{jsx as f}from"react/jsx-runtime";import{forwardRef as c}from"react";import{Button as p}from"react-aria-components";import{cn as g}from"../../utils/cn.js";function h(e){switch(e){case"primary":return"solid";case"secondary":return"outline";case"tertiary":return"ghost";default:return e}}const w=c(function({className:r,isDisabled:d=!1,isLoading:t=!1,size:s="md",variant:n="solid",...a},b){const o=h(n),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","focus:outline-2 focus:outline focus:outline-border-subtle pressed:scale-95",s==="sm"&&"h-8",s==="md"&&"h-10",s==="lg"&&"h-12",o==="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",t&&"disabled:text-fg"],o==="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",t&&["disabled:border-border","disabled:text-fg"]],o==="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",t&&["disabled:border-fg","disabled:text-white"]]];return f(p,{ref:b,type:"button",isDisabled:d,className:i=>{const l=typeof r=="function"?r(i):r;return g(u,l)},...a})});export{w as Button,w as default,h as resolveVariant};
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:a="solid",...d},b){const r=h(a),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","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)},...d})});export{w as Button,w as default,h as resolveVariant};
@@ -1,5 +1,5 @@
1
1
  import { ButtonProps as AriaButtonProps } from 'react-aria-components';
2
- export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'primary' | 'secondary' | 'tertiary';
2
+ export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'link' | 'primary' | 'secondary' | 'tertiary';
3
3
  export type ButtonProps = AriaButtonProps & {
4
4
  isLoading?: boolean;
5
5
  variant?: ButtonVariant;
@@ -1 +1 @@
1
- "use client";import{jsx as u}from"react/jsx-runtime";import{forwardRef as p}from"react";import{cn as c}from"../../utils/cn.js";import{Link as m}from"../Link/Link.js";import"../Link/LinkContext.js";import{resolveVariant as g}from"../Button/Button.js";import"react-aria-components";import"../Link/utils.js";const h=p(({className:e,isDisabled:o,isLoading:r=!1,size:t="md",variant:d="solid",children:a,...b},n)=>{const s=g(d),i=["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","focus:outline-2 focus:outline focus:outline-border-subtle pressed:scale-95",t==="sm"&&"h-8",t==="md"&&"h-10",t==="lg"&&"h-12",s==="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",r&&"disabled:text-fg"],s==="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",r&&["disabled:border-border","disabled:text-fg"]],s==="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 hover:text-white",r&&["disabled:border-fg","disabled:text-white"]]];return u(m,{ref:n,isDisabled:o,className:l=>{const f=typeof e=="function"?e(l):e;return c(i,f)},...b,children:a})});h.displayName="LinkAsButton";export{h as LinkAsButton,h as default};
1
+ "use client";import{jsx as u}from"react/jsx-runtime";import{forwardRef as p}from"react";import{cn as g}from"../../utils/cn.js";import{Link as c}from"../Link/Link.js";import"../Link/LinkContext.js";import{resolveVariant as m}from"../Button/Button.js";import"react-aria-components";import"../Link/utils.js";const h=p(({className:t,isDisabled:o,isLoading:e=!1,size:s="md",variant:d="solid",children:n,...a},b)=>{const r=m(d),i=["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","focus:outline-2 focus:outline focus:outline-border-subtle pressed:scale-95",s==="sm"&&"h-8",s==="md"&&"h-10",s==="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 hover:text-white",e&&["disabled:border-fg","disabled:text-white"]]];return u(c,{ref:b,isDisabled:o,className:l=>{const f=typeof t=="function"?t(l):t;return g(i,f)},...a,children:n})});h.displayName="LinkAsButton";export{h as LinkAsButton,h as default};
@@ -0,0 +1,116 @@
1
+ # IMPROVEMENT: `Button` and `LinkAsButton` gain a `link` variant
2
+
3
+ **Date:** 2026-09-01
4
+ **Package:** `@clicktap/ui`
5
+ **Affects:** `src/components/Button/Button.tsx`, `Button.types.ts`, `src/components/LinkAsButton/LinkAsButton.tsx`
6
+ **Classification:** IMPROVEMENT
7
+ **Breaking:** No — additive. `ButtonVariant` widens, which only affects code that
8
+ enumerates it exhaustively; see Consumer Impact.
9
+ **Ticket:** CTAP-1569
10
+
11
+ ## Problem
12
+
13
+ Some actions are secondary enough that a button is the wrong weight — "set as
14
+ default" sitting next to a card's edit/delete icons, for instance. Until now the
15
+ only way to render one was to take an `outline` button and argue it back into a
16
+ link at the call site:
17
+
18
+ ```tsx
19
+ <Button
20
+ variant="outline"
21
+ className={cn(
22
+ '!text-xs rounded-lg text-xs uppercase underline text-slate-800 p-0 data-[hovered]:text-slate-800',
23
+ 'border-0 outline-0 hover:border-0 focus:border-0 hover:outline-0 focus:outline-0',
24
+ )}
25
+ >
26
+ ```
27
+
28
+ Nine utilities, six of which exist only to cancel the variant that was just
29
+ asked for. ⚠️ That shape does not survive contact with a second call site: the
30
+ account area had this on My Addresses and a plain `variant="outline"` button on
31
+ both saved-card screens, so one action rendered three different ways. Nothing
32
+ flags the drift — every copy compiles and lints clean.
33
+
34
+ ## What changed
35
+
36
+ `ButtonVariant` gains `'link'`, and both `Button` and `LinkAsButton` style it:
37
+
38
+ ```tsx
39
+ <Button variant="link" onPress={setAsDefault}>Set as default</Button>
40
+ ```
41
+
42
+ It keeps the element (`<button>` for `Button`, `<a>` for `LinkAsButton`) and
43
+ sheds the button box:
44
+
45
+ | | `outline` | `link` |
46
+ |---|---|---|
47
+ | padding | `py-2 px-8` | `p-0` |
48
+ | height | `h-8` / `h-10` / `h-12` by `size` | `h-auto` — **`size` has no effect** |
49
+ | border | `border-border` | `border-transparent` |
50
+ | radius | `rounded-md` | `rounded-none` |
51
+ | decoration | none | `underline underline-offset-2` |
52
+ | hover | border darkens | `opacity-70` |
53
+
54
+ Two consequences worth knowing:
55
+
56
+ - **`size` is inert on `link`.** Its height comes from its own line box, so all
57
+ three sizes render identically. Change the inherited font size instead.
58
+ - **Hover is `opacity-70`, not a color change.** That is deliberate: a call site
59
+ that overrides the text color (`text-slate-800`) keeps a working hover without
60
+ also having to override a `hover:text-*` it did not ask for. This is what the
61
+ old workaround's `data-[hovered]:text-slate-800` was for.
62
+
63
+ `link` has no semantic alias. `primary`/`secondary`/`tertiary` still map to
64
+ `solid`/`outline`/`ghost`; there is no fourth alias, because a link is a
65
+ presentational choice rather than a rung on the emphasis ladder.
66
+
67
+ ## Consumer Impact
68
+
69
+ **No action needed to upgrade.** Existing variants are untouched — the spec pins
70
+ that `outline` still renders `px-8 h-8 border-border` and no underline.
71
+
72
+ **If you have a hand-rolled link-button**, replace it. The old workaround still
73
+ works, but it fights the base layer through `cn`/tailwind-merge and will keep
74
+ drifting between copies:
75
+
76
+ ```tsx
77
+ // BEFORE
78
+ <Button variant="outline" className={cn(
79
+ '!text-xs rounded-lg text-xs uppercase underline text-slate-800 p-0 data-[hovered]:text-slate-800',
80
+ 'border-0 outline-0 hover:border-0 focus:border-0 hover:outline-0 focus:outline-0',
81
+ )}>
82
+
83
+ // AFTER
84
+ <Button variant="link" className="text-xs uppercase text-slate-800">
85
+ ```
86
+
87
+ ⚠️ Drop `!text-xs` when you migrate. The `!important` was there to beat the base
88
+ `text-sm` through specificity; `cn` resolves that by merge order now, and a
89
+ lingering `!important` will beat your own later overrides too.
90
+
91
+ ⚠️ **If you enumerate `ButtonVariant` exhaustively** — a `switch` with no
92
+ `default`, a `Record<ButtonVariant, …>`, a radio-control options array — the
93
+ widened union is a compile error (`Record`) or a silent gap (`switch`). Both are
94
+ one-line fixes; `resolveVariant`'s return type widened to
95
+ `'solid' | 'outline' | 'ghost' | 'link'` for the same reason, so a consumer
96
+ narrowing on its result needs the new arm.
97
+
98
+ ## Verification
99
+
100
+ `src/components/Button/Button.spec.tsx` — 7 tests.
101
+
102
+ **Mutation-tested.** Deleting the `link` blocks from both components fails 4 of
103
+ 7; the 3 that survive are correct to survive (it renders a `<button>`, the other
104
+ variants stay boxed, a call-site class still wins — none of those depend on the
105
+ new variant existing).
106
+
107
+ The assertions are deliberately **cancellation** assertions: they check `px-8`,
108
+ `py-2`, `rounded-md` and `border-border` are ABSENT from the final className, not
109
+ merely that `underline` and `p-0` are present. ⚠️ A positive-only test passes
110
+ against a variant that appends link classes while leaving the button box behind
111
+ — which renders a padded, bordered, underlined box. That is the wrong answer this
112
+ suite exists to catch.
113
+
114
+ ⚠️ **Not visually verified against a brand theme.** The tests assert which
115
+ classes survive `cn` in jsdom. Whether the result reads as a link on a given
116
+ storefront's type scale is not something they can show.
@@ -1,4 +1,14 @@
1
1
  [
2
+ {
3
+ "path": "2026/09/ui-2026-09-01-button-link-variant.md",
4
+ "module": "libs/frontend-modules/ui",
5
+ "package": "@clicktap/ui",
6
+ "date": "2026-09-01",
7
+ "title": "IMPROVEMENT: `Button` and `LinkAsButton` gain a `link` variant",
8
+ "classification": "IMPROVEMENT",
9
+ "breaking": "No — additive. `ButtonVariant` widens, which only affects code that",
10
+ "affects": "`src/components/Button/Button.tsx`, `Button.types.ts`, `src/components/LinkAsButton/LinkAsButton.tsx`"
11
+ },
2
12
  {
3
13
  "path": "2026/08/ui-2026-08-26-badge-classnames-badge-targets-the-pill.md",
4
14
  "module": "libs/frontend-modules/ui",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clicktap/ui",
3
- "version": "2.0.10",
3
+ "version": "2.1.0",
4
4
  "private": false,
5
5
  "author": "Clicktap",
6
6
  "description": "A library of React UI components and low-level hooks.",