@cosxai/ui 1.0.0-alpha.7 → 1.0.0-alpha.9

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 CHANGED
@@ -1,3 +1,16 @@
1
+ ## 1.0.0-alpha.9 (2026-09-30)
2
+
3
+ - WorkspaceRow: `badge` — what the workspace still needs, as an attention
4
+ badge beside the chevron ("Needs setup"; Metaroom Sign In, Set up your
5
+ workspace).
6
+
7
+ ## 1.0.0-alpha.8 (2026-09-30)
8
+
9
+ - ThemeSwitch: `defaultOpen` starts it opened (documentation specimens).
10
+ - WorkspaceMark: a logo that failed before React hydrated (a server-rendered
11
+ page) now falls back to the initial too — onError had already fired.
12
+ - design.cosx.co: Patterns · Sign-in, every specimen the real component.
13
+
1
14
  ## 1.0.0-alpha.7 (2026-09-30)
2
15
 
3
16
  Sign-in patterns checked against design.cosx.co Patterns · Sign-in:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosxai/ui",
3
- "version": "1.0.0-alpha.7",
3
+ "version": "1.0.0-alpha.9",
4
4
  "description": "COSX Design System 3.0 \u2014 tokens, Tailwind v4 theme and React components",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -21,6 +21,8 @@ export type ThemeSwitchProps = Omit<ComponentProps<'div'>, 'onChange' | 'childre
21
21
  /** Unset: the ThemeProvider's setter. */
22
22
  onChange?: ((mode: ThemeMode) => void) | undefined;
23
23
  labels?: ThemeSwitchLabels | undefined;
24
+ /** Start opened (documentation specimens); it folds once the pointer leaves. */
25
+ defaultOpen?: boolean | undefined;
24
26
  };
25
27
 
26
28
  /** A click on an option only picks it once the switch has been open this long. */
@@ -33,11 +35,11 @@ const PICK_AFTER_MS = 800;
33
35
  * so the first tap on touch (which opens it) steps instead of landing on
34
36
  * whatever slid under the finger.
35
37
  */
36
- export function ThemeSwitch({ value, onChange, labels, className, ...rest }: ThemeSwitchProps) {
38
+ export function ThemeSwitch({ value, onChange, labels, defaultOpen = false, className, ...rest }: ThemeSwitchProps) {
37
39
  const theme = useTheme();
38
40
  const mode = value ?? theme.mode;
39
41
  const setMode = onChange ?? theme.setMode;
40
- const [open, setOpen] = useState(false);
42
+ const [open, setOpen] = useState(defaultOpen);
41
43
  const openedAt = useRef(0);
42
44
  const show = () => {
43
45
  if (open) return;
@@ -1,7 +1,8 @@
1
1
  import { ChevronRight } from 'lucide-react';
2
- import { forwardRef, useState, type ComponentProps, type ReactNode } from 'react';
2
+ import { forwardRef, useEffect, useRef, useState, type ComponentProps, type ReactNode } from 'react';
3
3
 
4
4
  import { cn } from '../lib/cn';
5
+ import { Badge } from './Badge';
5
6
  import { Spinner } from './Spinner';
6
7
 
7
8
  export type WorkspaceMarkProps = {
@@ -15,8 +16,15 @@ export type WorkspaceMarkProps = {
15
16
  /** WorkspaceMark — a workspace's logo, or its initial on the brand yellow. */
16
17
  export function WorkspaceMark({ name, logoUrl, size = 36 }: WorkspaceMarkProps) {
17
18
  const [failed, setFailed] = useState(false);
19
+ const img = useRef<HTMLImageElement>(null);
20
+ // A server-rendered page can fail the image before React is listening:
21
+ // onError never fires, so look once it is.
22
+ useEffect(() => {
23
+ const el = img.current;
24
+ if (el?.complete && el.naturalWidth === 0) setFailed(true);
25
+ }, [logoUrl]);
18
26
  if (logoUrl && !failed) {
19
- return <img src={logoUrl} alt="" aria-hidden style={{ width: size, height: size }} className="shrink-0 rounded-[8px] object-cover" onError={() => setFailed(true)} />;
27
+ return <img ref={img} src={logoUrl} alt="" aria-hidden style={{ width: size, height: size }} className="shrink-0 rounded-[8px] object-cover" onError={() => setFailed(true)} />;
20
28
  }
21
29
  return (
22
30
  <span aria-hidden style={{ width: size, height: size }} className="grid shrink-0 place-items-center rounded-[8px] bg-brand-field text-[13px] font-semibold text-ink">
@@ -32,14 +40,17 @@ export type WorkspaceRowProps = Omit<ComponentProps<'button'>, 'children'> & {
32
40
  logoUrl?: string | undefined;
33
41
  /** Being opened: a spinner in place of the chevron. */
34
42
  busy?: boolean | undefined;
43
+ /** A state beside the chevron, as an attention badge: "Needs setup". */
44
+ badge?: ReactNode;
35
45
  };
36
46
 
37
47
  /**
38
- * WorkspaceRow — one workspace in a chooser: mark, name, detail, chevron.
48
+ * WorkspaceRow — one workspace in a chooser: mark, name, detail, chevron;
49
+ * `badge` says what it still needs ("Needs setup").
39
50
  * The whole row is the button; `busy` shows it opening (disable the others).
40
51
  */
41
52
  export const WorkspaceRow = forwardRef<HTMLButtonElement, WorkspaceRowProps>(function WorkspaceRow(
42
- { name, detail, logoUrl, busy = false, className, ...rest },
53
+ { name, detail, logoUrl, busy = false, badge, className, ...rest },
43
54
  ref,
44
55
  ) {
45
56
  return (
@@ -58,6 +69,11 @@ export const WorkspaceRow = forwardRef<HTMLButtonElement, WorkspaceRowProps>(fun
58
69
  <span className="truncate text-[14px] font-medium">{name}</span>
59
70
  {detail && <span className="truncate text-[12px] text-fg-secondary">{detail}</span>}
60
71
  </span>
72
+ {badge && (
73
+ <span className="flex shrink-0">
74
+ <Badge status="attention">{badge}</Badge>
75
+ </span>
76
+ )}
61
77
  {busy ? <Spinner size={16} /> : <ChevronRight size={16} aria-hidden className="text-fg-secondary" />}
62
78
  </button>
63
79
  );