@cosxai/ui 0.4.6 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosxai/ui",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
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,15 +1,33 @@
1
- import { useContext, useEffect, useMemo } from "react";
1
+ import { useContext, useEffect } from "react";
2
2
  import { ActionBarContext } from "./actionbar-context";
3
3
  import type { ActionBarItem } from "./types";
4
4
 
5
5
  // Register a set of items into the global action-bar registry.
6
6
  // Auto-unregisters on unmount + on sourceKey change.
7
7
  //
8
- // `items` is the live array — pass a useMemo'd array to avoid
9
- // re-registration thrash. The provider's register() does shallow
10
- // equality, so passing a NEW array with identical refs is fine,
11
- // but a NEW array with all-new item objects (from inline JSX
12
- // blocks) will re-register every render.
8
+ // **Pass a `useMemo`'d array.** The effect re-registers whenever
9
+ // the array identity changes; the provider's `register()` then
10
+ // dedups by item-by-item identity, so a fresh array with identical
11
+ // item refs is cheap (state stays === and no re-render fires) but
12
+ // a fresh array with NEW item objects (e.g. consumers whose
13
+ // onClick closures captured updated state) does re-register —
14
+ // downstream renders pick up the fresh handlers.
15
+ //
16
+ // Inline-array consumers (`useActionBarItems(key, [{...}])`)
17
+ // will allocate new items every render and loop infinitely
18
+ // through `register → setState → re-render → register`. The
19
+ // useMemo on the caller side is what keeps that loop bounded
20
+ // to "items whose deps actually changed".
21
+ //
22
+ // Previous versions wrapped items in a local `useMemo` keyed by
23
+ // `[items.length, items.map(i => i.key).join('|')]`. That gate
24
+ // suppressed identity updates whenever the keys + length stayed
25
+ // stable, which is precisely the case for a selection-mode
26
+ // toolbar whose buttons are fixed but whose onClicks close over
27
+ // changing state — they ended up registered ONCE with the first
28
+ // render's closures and never refreshed, shipping stale state to
29
+ // every subsequent click. The gate is gone; the provider's
30
+ // shallow item-identity dedup is the single source of truth.
13
31
  //
14
32
  // **Important**: the useEffect deps below intentionally do NOT
15
33
  // include the ctx object itself — only its stable function refs
@@ -27,15 +45,10 @@ export function useActionBarItems(sourceKey: string, items: ActionBarItem[]) {
27
45
  throw new Error("useActionBarItems must be used within <ActionBarProvider>");
28
46
  }
29
47
  const { register, unregister } = ctx;
30
- const memo = useMemo(
31
- () => items,
32
- // eslint-disable-next-line react-hooks/exhaustive-deps
33
- [items.length, items.map((i) => i.key).join("|")],
34
- );
35
48
  useEffect(() => {
36
- register(sourceKey, memo);
49
+ register(sourceKey, items);
37
50
  return () => unregister(sourceKey);
38
- }, [register, unregister, sourceKey, memo]);
51
+ }, [register, unregister, sourceKey, items]);
39
52
  }
40
53
 
41
54
  export function useActionBarItemsContext() {
@@ -161,6 +161,14 @@ export function ModalHeader({ title, subtitle, onClose, titleId }: ModalHeaderPr
161
161
  type="button"
162
162
  onClick={onClose}
163
163
  aria-label="Close"
164
+ onMouseEnter={(e) => {
165
+ (e.currentTarget as HTMLButtonElement).style.background =
166
+ "var(--ck-bg-muted)";
167
+ }}
168
+ onMouseLeave={(e) => {
169
+ (e.currentTarget as HTMLButtonElement).style.background =
170
+ "transparent";
171
+ }}
164
172
  style={{
165
173
  border: "none",
166
174
  background: "transparent",
@@ -168,7 +176,10 @@ export function ModalHeader({ title, subtitle, onClose, titleId }: ModalHeaderPr
168
176
  cursor: "pointer",
169
177
  fontSize: 18,
170
178
  lineHeight: 1,
171
- padding: 2,
179
+ padding: 4,
180
+ borderRadius: "var(--ck-radius-sm, 4px)",
181
+ transition:
182
+ "background var(--ck-dur-fast) var(--ck-ease)",
172
183
  }}
173
184
  >
174
185
  ×
@@ -116,6 +116,18 @@ html[data-ck-chrome="editorial"] [data-ck-leftnav] {
116
116
  border-right-color: var(--ck-border-subtle);
117
117
  }
118
118
 
119
+ /* Nav-item hover — match the actionbar / button hover token so
120
+ every interactive surface in the editorial chrome reads as
121
+ actionable on pointer enter. The sketch / ambient / riso / etc.
122
+ chromes have always carried this rule; editorial was the only
123
+ gap (consumers saw rail items as static even though they're
124
+ navigable). Active rows keep their accent-muted background from
125
+ the base NavItem inline style, so the `:not([data-active="true"])`
126
+ guard is the same shape the other chromes use. */
127
+ html[data-ck-chrome="editorial"] [data-ck-navitem]:not([data-active="true"]):hover {
128
+ background: var(--ck-bg-muted);
129
+ }
130
+
119
131
  /* Topbar — flatten to the paper, lose the bottom border (the
120
132
  folio rule under it is the visual divider in editorial). */
121
133
  html[data-ck-chrome="editorial"] [data-ck-topbar] {