@cosxai/ui 1.0.0-alpha.2 → 1.0.0-alpha.3
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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/components/Button.tsx +31 -38
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 1.0.0-alpha.3 (2026-09-29)
|
|
2
|
+
|
|
3
|
+
- Button: busy and done draw over the idle content, kept in place and
|
|
4
|
+
hidden, so the width never changes — also when the button first
|
|
5
|
+
renders busy (it used to shrink to the spinner). No width measuring.
|
|
6
|
+
|
|
1
7
|
## 1.0.0-alpha.2 (2026-09-29)
|
|
2
8
|
|
|
3
9
|
MetaRoom product patterns (spec P0), checked light / ink × en / zh.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Slot } from '@radix-ui/react-slot';
|
|
2
2
|
import { cva } from 'class-variance-authority';
|
|
3
3
|
import { Check } from 'lucide-react';
|
|
4
|
-
import {
|
|
4
|
+
import type { ComponentProps, ReactNode } from 'react';
|
|
5
5
|
|
|
6
6
|
import { cn } from '../lib/cn';
|
|
7
7
|
import { Spinner } from './Spinner';
|
|
@@ -76,7 +76,7 @@ export type ButtonProps = Omit<ComponentProps<'button'>, 'disabled'> & {
|
|
|
76
76
|
shortcut?: string | undefined;
|
|
77
77
|
/** A count at the end (Comments 12). Hidden at zero, 99+ above 99. */
|
|
78
78
|
count?: number | undefined;
|
|
79
|
-
/** Async state: busy
|
|
79
|
+
/** Async state: busy shows a spinner, done confirms in place; the width never changes. See useButtonAction. */
|
|
80
80
|
state?: ButtonState | undefined;
|
|
81
81
|
/** Label shown while state="done". @default the children */
|
|
82
82
|
doneLabel?: ReactNode;
|
|
@@ -119,15 +119,6 @@ export function Button({
|
|
|
119
119
|
ref,
|
|
120
120
|
...rest
|
|
121
121
|
}: ButtonProps) {
|
|
122
|
-
const inner = useRef<HTMLButtonElement | null>(null);
|
|
123
|
-
const [lockedWidth, setLockedWidth] = useState<number | null>(null);
|
|
124
|
-
|
|
125
|
-
// Lock the idle width so busy / done never resize the button.
|
|
126
|
-
useLayoutEffect(() => {
|
|
127
|
-
if (state === 'idle') setLockedWidth(null);
|
|
128
|
-
else if (lockedWidth === null && inner.current) setLockedWidth(inner.current.getBoundingClientRect().width);
|
|
129
|
-
}, [state, lockedWidth]);
|
|
130
|
-
|
|
131
122
|
const classes = cn(button({ variant, size, ground: ground ?? 'auto' }), className);
|
|
132
123
|
if (asChild) {
|
|
133
124
|
return (
|
|
@@ -138,21 +129,18 @@ export function Button({
|
|
|
138
129
|
}
|
|
139
130
|
|
|
140
131
|
const busy = state === 'busy';
|
|
132
|
+
// Busy and done keep the idle content in place (hidden) and draw on top of
|
|
133
|
+
// it, so the button never resizes — also when it first renders busy.
|
|
134
|
+
const overlaid = state !== 'idle';
|
|
141
135
|
const blocked = Boolean(disabledReason) && disabled;
|
|
142
136
|
const shownCount = count === undefined ? null : formatCount(count);
|
|
143
|
-
const setRefs = (el: HTMLButtonElement | null) => {
|
|
144
|
-
inner.current = el;
|
|
145
|
-
if (typeof ref === 'function') ref(el);
|
|
146
|
-
else if (ref) ref.current = el;
|
|
147
|
-
};
|
|
148
|
-
|
|
149
137
|
return (
|
|
150
138
|
<button
|
|
151
139
|
type="button"
|
|
152
140
|
{...rest}
|
|
153
|
-
ref={
|
|
141
|
+
ref={ref}
|
|
154
142
|
className={classes}
|
|
155
|
-
style={
|
|
143
|
+
style={style}
|
|
156
144
|
disabled={disabled && !disabledReason ? true : undefined}
|
|
157
145
|
// Busy is not disabled (spec: "busy and disabled are different states"):
|
|
158
146
|
// it keeps its look; aria-busy says so and repeat clicks are ignored below.
|
|
@@ -167,27 +155,32 @@ export function Button({
|
|
|
167
155
|
onClick?.(e);
|
|
168
156
|
}}
|
|
169
157
|
>
|
|
170
|
-
{
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
{
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
158
|
+
<span className={cn('contents', overlaid && 'invisible')} aria-hidden={overlaid || undefined} data-button-content="">
|
|
159
|
+
{iconLeft}
|
|
160
|
+
{children}
|
|
161
|
+
{iconRight}
|
|
162
|
+
{shownCount && <span className="font-medium tabular-nums opacity-70">{shownCount}</span>}
|
|
163
|
+
{shortcut && (
|
|
164
|
+
<kbd className="ml-0.5 font-sans text-[12px] font-medium opacity-60" aria-hidden>
|
|
165
|
+
{shortcut}
|
|
166
|
+
</kbd>
|
|
167
|
+
)}
|
|
168
|
+
</span>
|
|
169
|
+
{overlaid && (
|
|
170
|
+
<span className="absolute inset-0 flex items-center justify-center gap-2" data-button-overlay="">
|
|
171
|
+
{busy ? (
|
|
172
|
+
<>
|
|
173
|
+
<Spinner />
|
|
174
|
+
<span className="sr-only">{children}</span>
|
|
175
|
+
</>
|
|
176
|
+
) : (
|
|
177
|
+
<>
|
|
178
|
+
<Check size={14} strokeWidth={2} aria-hidden />
|
|
179
|
+
{doneLabel ?? children}
|
|
180
|
+
</>
|
|
187
181
|
)}
|
|
188
|
-
|
|
182
|
+
</span>
|
|
189
183
|
)}
|
|
190
|
-
{busy && <span className="sr-only">{children}</span>}
|
|
191
184
|
</button>
|
|
192
185
|
);
|
|
193
186
|
}
|