@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,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosxai/ui",
3
- "version": "0.4.10",
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",
@@ -76,6 +76,7 @@ export function ActionBarButton({
76
76
  </span>
77
77
  {hint && (
78
78
  <span
79
+ className="ck-actionbar-hint"
79
80
  style={{
80
81
  color: isPrimary ? "rgba(255,255,255,0.7)" : "var(--ck-text-tertiary)",
81
82
  font: "500 11px/1 var(--ck-font-mono)",
@@ -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);
@@ -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 md breakpoint).
233
- Icons + tooltips remain — the bar's still informative on phones. */
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
  }