@cosxai/ui 1.0.0-alpha.6 → 1.0.0-alpha.8

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,20 @@
1
+ ## 1.0.0-alpha.8 (2026-09-30)
2
+
3
+ - ThemeSwitch: `defaultOpen` starts it opened (documentation specimens).
4
+ - WorkspaceMark: a logo that failed before React hydrated (a server-rendered
5
+ page) now falls back to the initial too — onError had already fired.
6
+ - design.cosx.co: Patterns · Sign-in, every specimen the real component.
7
+
8
+ ## 1.0.0-alpha.7 (2026-09-30)
9
+
10
+ Sign-in patterns checked against design.cosx.co Patterns · Sign-in:
11
+
12
+ - AuthPanel: the highlight band runs 40–94% with a hair of overhang each
13
+ side; Chinese sets at 1.35, untracked, band from 50% (typography-cjk's
14
+ :lang(zh) line-height is unlayered, so this one is important).
15
+ - WorkspaceRow: 14px name at 1.35, the outline an inset rule on the page
16
+ colour (no border box).
17
+
1
18
  ## 1.0.0-alpha.6 (2026-09-29)
2
19
 
3
20
  Sign-in patterns (Metaroom sign-in, design.cosx.co), moved here from the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosxai/ui",
3
- "version": "1.0.0-alpha.6",
3
+ "version": "1.0.0-alpha.8",
4
4
  "description": "COSX Design System 3.0 \u2014 tokens, Tailwind v4 theme and React components",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -69,9 +69,13 @@ export function AuthPanel({ eyebrow, lead, mark, sub, logo }: AuthPanelProps) {
69
69
  {logo ?? <Logo height={22} tone="ink" />}
70
70
  <div className="flex flex-col gap-4">
71
71
  <span className="text-[13px] font-medium text-[rgba(17,17,17,.7)]">{eyebrow}</span>
72
- <p className="m-0 text-[34px] leading-[1.22] font-medium tracking-[-.02em] text-pretty">
72
+ {/* Chinese sets at 1.35, untracked, its band starting lower (design: Patterns · Sign-in).
73
+ Important: typography-cjk.css sets every :lang(zh) line-height outside the layers. */}
74
+ <p className="m-0 text-[34px] leading-[1.22] font-medium tracking-[-.02em] text-pretty [&:lang(zh)]:leading-[1.35]! [&:lang(zh)]:tracking-normal!">
73
75
  {lead}
74
- <span className="box-decoration-clone bg-[linear-gradient(transparent_45%,var(--brand-mark)_45%,var(--brand-mark)_95%,transparent_95%)]">{mark}</span>
76
+ <span className="mx-[-.08em] box-decoration-clone px-[.08em] bg-[linear-gradient(transparent_40%,var(--brand-mark)_40%,var(--brand-mark)_94%,transparent_94%)] [&:lang(zh)]:bg-[linear-gradient(transparent_50%,var(--brand-mark)_50%,var(--brand-mark)_94%,transparent_94%)]">
77
+ {mark}
78
+ </span>
75
79
  </p>
76
80
  {sub && <p className="m-0 text-[15px] leading-[1.6] text-[rgba(17,17,17,.7)]">{sub}</p>}
77
81
  </div>
@@ -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,5 +1,5 @@
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
5
  import { Spinner } from './Spinner';
@@ -15,8 +15,15 @@ export type WorkspaceMarkProps = {
15
15
  /** WorkspaceMark — a workspace's logo, or its initial on the brand yellow. */
16
16
  export function WorkspaceMark({ name, logoUrl, size = 36 }: WorkspaceMarkProps) {
17
17
  const [failed, setFailed] = useState(false);
18
+ const img = useRef<HTMLImageElement>(null);
19
+ // A server-rendered page can fail the image before React is listening:
20
+ // onError never fires, so look once it is.
21
+ useEffect(() => {
22
+ const el = img.current;
23
+ if (el?.complete && el.naturalWidth === 0) setFailed(true);
24
+ }, [logoUrl]);
18
25
  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)} />;
26
+ 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
27
  }
21
28
  return (
22
29
  <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">
@@ -48,14 +55,14 @@ export const WorkspaceRow = forwardRef<HTMLButtonElement, WorkspaceRowProps>(fun
48
55
  type="button"
49
56
  aria-busy={busy || undefined}
50
57
  className={cn(
51
- 'flex w-full cursor-pointer items-center gap-3 rounded-[12px] border border-rule bg-transparent px-3.5 py-3 text-left text-fg outline-none transition-colors duration-[120ms] hover:bg-hover focus-visible:shadow-(--focus-ring) disabled:cursor-default',
58
+ 'flex w-full cursor-pointer items-center gap-3 rounded-[12px] border-0 bg-page px-3.5 py-3 text-left text-fg shadow-[inset_0_0_0_1px_var(--rule)] outline-none transition-colors duration-[120ms] hover:bg-hover focus-visible:shadow-(--focus-ring) disabled:cursor-default',
52
59
  className,
53
60
  )}
54
61
  {...rest}
55
62
  >
56
63
  <WorkspaceMark name={name} logoUrl={logoUrl} />
57
- <span className="flex min-w-0 flex-1 flex-col">
58
- <span className="truncate text-[15px] font-medium">{name}</span>
64
+ <span className="flex min-w-0 flex-1 flex-col leading-[1.35]">
65
+ <span className="truncate text-[14px] font-medium">{name}</span>
59
66
  {detail && <span className="truncate text-[12px] text-fg-secondary">{detail}</span>}
60
67
  </span>
61
68
  {busy ? <Spinner size={16} /> : <ChevronRight size={16} aria-hidden className="text-fg-secondary" />}