@cosxai/ui 0.4.10 → 0.5.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.
package/package.json
CHANGED
|
@@ -1,30 +1,67 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
|
|
2
2
|
import { cn } from "../lib/cn";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Button — primitive interactive element. Variants `primary`
|
|
6
|
+
* (filled) / `secondary` (outlined) / `ghost` (transparent) /
|
|
7
|
+
* `icon` (square 32×32).
|
|
8
|
+
*
|
|
9
|
+
* The `loading` prop is the canonical way to show that an async
|
|
10
|
+
* action triggered by the button is in flight. When true:
|
|
11
|
+
*
|
|
12
|
+
* - a small ring spinner renders before the children (inherits
|
|
13
|
+
* `currentColor` so it reads on every variant),
|
|
14
|
+
* - the button is `disabled` natively so clicks are dropped,
|
|
15
|
+
* - `aria-busy="true"` is set for AT consumers.
|
|
16
|
+
*
|
|
17
|
+
* `loading` and `disabled` are distinct states. `disabled` says
|
|
18
|
+
* "you can't take this action right now" (form invalid, no
|
|
19
|
+
* selection, etc.). `loading` says "we're already taking this
|
|
20
|
+
* action — wait". Callers typically wire both:
|
|
21
|
+
*
|
|
22
|
+
* <Button disabled={!name.trim()} loading={submitting}>
|
|
23
|
+
* {submitting ? "Saving…" : "Save"}
|
|
24
|
+
* </Button>
|
|
25
|
+
*
|
|
26
|
+
* Visual: see docs/components/button.
|
|
27
|
+
*
|
|
28
|
+
* Forwards ref to the underlying <button>. Spreads `...rest` so
|
|
29
|
+
* consumers can pass data-* / aria-* / className.
|
|
30
|
+
*/
|
|
8
31
|
|
|
9
32
|
export type ButtonVariant = "primary" | "secondary" | "ghost" | "icon";
|
|
10
33
|
|
|
11
34
|
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
12
|
-
variant?: ButtonVariant;
|
|
35
|
+
variant?: ButtonVariant | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* When true, the button renders a leading spinner, sets
|
|
38
|
+
* `aria-busy="true"`, and becomes natively `disabled` so click
|
|
39
|
+
* handlers no longer fire. Use this for any async submit so the
|
|
40
|
+
* user sees the click landed AND can't double-fire the action.
|
|
41
|
+
*/
|
|
42
|
+
loading?: boolean | undefined;
|
|
13
43
|
children?: ReactNode;
|
|
14
44
|
}
|
|
15
45
|
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
46
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
47
|
+
function Button(
|
|
48
|
+
{ variant = "primary", className, children, loading, disabled, ...rest },
|
|
49
|
+
ref,
|
|
50
|
+
) {
|
|
51
|
+
return (
|
|
52
|
+
<button
|
|
53
|
+
ref={ref}
|
|
54
|
+
{...rest}
|
|
55
|
+
className={cn("ck-btn", `ck-btn--${variant}`, className)}
|
|
56
|
+
disabled={disabled || loading}
|
|
57
|
+
aria-busy={loading || undefined}
|
|
58
|
+
data-loading={loading || undefined}
|
|
59
|
+
>
|
|
60
|
+
{loading ? (
|
|
61
|
+
<span className="ck-btn-spinner" aria-hidden="true" />
|
|
62
|
+
) : null}
|
|
63
|
+
{children}
|
|
64
|
+
</button>
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
);
|
package/src/styles/index.css
CHANGED
|
@@ -39,6 +39,30 @@
|
|
|
39
39
|
|
|
40
40
|
.ck-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
41
41
|
|
|
42
|
+
/* Loading spinner — rendered inside a Button when its `loading`
|
|
43
|
+
prop is true. CSS-only ring (1.5px stroke, 720ms linear spin).
|
|
44
|
+
`currentColor` inherits the variant's foreground so the spinner
|
|
45
|
+
reads on primary / secondary / ghost / icon without per-variant
|
|
46
|
+
overrides. Reduced-motion users get a slower spin instead of a
|
|
47
|
+
frozen ring (which would read as broken). */
|
|
48
|
+
.ck-btn-spinner {
|
|
49
|
+
width: 0.85em;
|
|
50
|
+
height: 0.85em;
|
|
51
|
+
border: 1.5px solid currentColor;
|
|
52
|
+
border-right-color: transparent;
|
|
53
|
+
border-radius: 50%;
|
|
54
|
+
display: inline-block;
|
|
55
|
+
vertical-align: -0.1em;
|
|
56
|
+
flex-shrink: 0;
|
|
57
|
+
animation: ck-btn-spin 720ms linear infinite;
|
|
58
|
+
}
|
|
59
|
+
@keyframes ck-btn-spin {
|
|
60
|
+
to { transform: rotate(360deg); }
|
|
61
|
+
}
|
|
62
|
+
@media (prefers-reduced-motion: reduce) {
|
|
63
|
+
.ck-btn-spinner { animation-duration: 2400ms; }
|
|
64
|
+
}
|
|
65
|
+
|
|
42
66
|
.ck-btn--primary {
|
|
43
67
|
background: var(--ck-accent);
|
|
44
68
|
color: var(--ck-text-inverse);
|
|
@@ -229,10 +253,13 @@ html[data-ck-chrome="seamless"] .ck-card__foot {
|
|
|
229
253
|
transform: translateY(-1px);
|
|
230
254
|
}
|
|
231
255
|
|
|
232
|
-
/* Hide labels below 768 px (the standard
|
|
233
|
-
Icons + tooltips remain — the bar's still
|
|
256
|
+
/* Hide labels + keyboard-shortcut hints below 768 px (the standard
|
|
257
|
+
md breakpoint). Icons + tooltips remain — the bar's still
|
|
258
|
+
informative on phones, and there's no physical keyboard so the
|
|
259
|
+
shortcut badge has nothing to advertise. */
|
|
234
260
|
@media (max-width: 767px) {
|
|
235
|
-
.ck-actionbar-label
|
|
261
|
+
.ck-actionbar-label,
|
|
262
|
+
.ck-actionbar-hint {
|
|
236
263
|
display: none;
|
|
237
264
|
}
|
|
238
265
|
}
|