@cosxai/ui 0.4.11 → 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,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosxai/ui",
3
- "version": "0.4.11",
3
+ "version": "0.5.0",
4
4
  "description": "COSX design system — React 19 component primitives shared across product-meta and other consumers",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -1,30 +1,67 @@
1
- import type { ButtonHTMLAttributes, ReactNode } from "react";
1
+ import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
2
2
  import { cn } from "../lib/cn";
3
3
 
4
- // Minimal Phase-0 stub so the docs app has something concrete to
5
- // importproves the workspace bridge + hot-reload. Phase 3
6
- // replaces this with the full variant + size + loading-state
7
- // implementation backed by the .rd-btn styles.
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 function Button({
17
- variant = "primary",
18
- className,
19
- children,
20
- ...rest
21
- }: ButtonProps) {
22
- return (
23
- <button
24
- {...rest}
25
- className={cn("ck-btn", `ck-btn--${variant}`, className)}
26
- >
27
- {children}
28
- </button>
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
+ );
@@ -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);