@cosxai/ui 0.19.0 → 0.20.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.19.0",
3
+ "version": "0.20.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",
@@ -87,10 +87,17 @@ export function CommandPalette({
87
87
  // Flat array for keyboard navigation (sequential across groups).
88
88
  const flat = useMemo(() => grouped.flatMap((g) => g.items), [grouped]);
89
89
 
90
- // Clamp selectedIdx when results shrink.
91
- useEffect(() => {
92
- if (selectedIdx >= flat.length) setSelectedIdx(Math.max(0, flat.length - 1));
93
- }, [flat.length, selectedIdx]);
90
+ // Effective selection: `selectedIdx` is the raw state the keyboard/mouse
91
+ // set, but disabled rows and a shrunk list can leave it pointing at a
92
+ // non-interactive or out-of-range item. Derive the index actually shown
93
+ // as selected — the first enabled row when the raw one isn't usable — so
94
+ // a disabled item (e.g. a "coming soon" teaser) is never highlighted and
95
+ // the default lands on the first real result.
96
+ const selIdx = useMemo(() => {
97
+ if (flat.length === 0) return 0;
98
+ if (selectedIdx < flat.length && !flat[selectedIdx]?.disabled) return selectedIdx;
99
+ return firstEnabled(flat);
100
+ }, [flat, selectedIdx]);
94
101
 
95
102
  const close = () => setOpen(false);
96
103
 
@@ -104,34 +111,34 @@ export function CommandPalette({
104
111
  }
105
112
  if (e.key === "ArrowDown") {
106
113
  e.preventDefault();
107
- setSelectedIdx((i) => (i + 1) % Math.max(1, flat.length));
114
+ setSelectedIdx(stepEnabled(flat, selIdx, 1));
108
115
  return;
109
116
  }
110
117
  if (e.key === "ArrowUp") {
111
118
  e.preventDefault();
112
- setSelectedIdx((i) => (i - 1 + Math.max(1, flat.length)) % Math.max(1, flat.length));
119
+ setSelectedIdx(stepEnabled(flat, selIdx, -1));
113
120
  return;
114
121
  }
115
122
  if (e.key === "Enter") {
116
123
  e.preventDefault();
117
- const pick = flat[selectedIdx];
118
- if (pick) pick.run({ close });
124
+ const pick = flat[selIdx];
125
+ if (pick && !pick.disabled) pick.run({ close });
119
126
  return;
120
127
  }
121
128
  };
122
129
  window.addEventListener("keydown", onKey);
123
130
  return () => window.removeEventListener("keydown", onKey);
124
- // close + flat + selectedIdx are stable enough; intentionally omit
125
- // close from deps so we don't re-bind every render.
131
+ // close + flat + selIdx are stable enough; intentionally omit close
132
+ // from deps so we don't re-bind every render.
126
133
  // eslint-disable-next-line react-hooks/exhaustive-deps
127
- }, [open, flat, selectedIdx]);
134
+ }, [open, flat, selIdx]);
128
135
 
129
136
  // Scroll selected row into view.
130
137
  useEffect(() => {
131
138
  if (!open) return;
132
- const node = listRef.current?.querySelector(`[data-row-idx="${selectedIdx}"]`);
139
+ const node = listRef.current?.querySelector(`[data-row-idx="${selIdx}"]`);
133
140
  (node as HTMLElement | null)?.scrollIntoView({ block: "nearest" });
134
- }, [open, selectedIdx]);
141
+ }, [open, selIdx]);
135
142
 
136
143
  if (!open) return null;
137
144
 
@@ -218,13 +225,15 @@ export function CommandPalette({
218
225
  </div>
219
226
  {g.items.map((it) => {
220
227
  const idx = flat.indexOf(it);
221
- const selected = idx === selectedIdx;
228
+ const disabled = !!it.disabled;
229
+ const selected = idx === selIdx && !disabled;
222
230
  return (
223
231
  <div
224
232
  key={it.key}
225
233
  data-row-idx={idx}
226
- onMouseEnter={() => setSelectedIdx(idx)}
227
- onClick={() => it.run({ close })}
234
+ aria-disabled={disabled || undefined}
235
+ onMouseEnter={disabled ? undefined : () => setSelectedIdx(idx)}
236
+ onClick={disabled ? undefined : () => it.run({ close })}
228
237
  style={{
229
238
  display: "flex",
230
239
  alignItems: "center",
@@ -232,8 +241,13 @@ export function CommandPalette({
232
241
  padding: "8px 12px",
233
242
  borderRadius: "var(--ck-radius-sm)",
234
243
  background: selected ? "var(--ck-accent-muted)" : "transparent",
235
- color: selected ? "var(--ck-accent)" : "var(--ck-text-primary)",
236
- cursor: "pointer",
244
+ color: disabled
245
+ ? "var(--ck-text-tertiary)"
246
+ : selected
247
+ ? "var(--ck-accent)"
248
+ : "var(--ck-text-primary)",
249
+ opacity: disabled ? 0.6 : 1,
250
+ cursor: disabled ? "default" : "pointer",
237
251
  font: "400 13px/1.3 var(--ck-font-sans)",
238
252
  }}
239
253
  >
@@ -293,3 +307,23 @@ export function CommandPalette({
293
307
  document.body,
294
308
  );
295
309
  }
310
+
311
+ // First index whose item isn't disabled, or 0 if every item is (or the
312
+ // list is empty) — the palette has to point somewhere.
313
+ function firstEnabled(list: CommandItem[]): number {
314
+ const i = list.findIndex((it) => !it.disabled);
315
+ return i === -1 ? 0 : i;
316
+ }
317
+
318
+ // Step from `from` in `dir` (+1 down / -1 up), wrapping, skipping disabled
319
+ // rows. Returns `from` when there's no other enabled row to land on.
320
+ function stepEnabled(list: CommandItem[], from: number, dir: 1 | -1): number {
321
+ const n = list.length;
322
+ if (n === 0) return 0;
323
+ let i = from;
324
+ for (let step = 0; step < n; step++) {
325
+ i = (i + dir + n) % n;
326
+ if (!list[i]?.disabled) return i;
327
+ }
328
+ return from;
329
+ }
@@ -29,4 +29,9 @@ export interface CommandItem {
29
29
  // matches. Use for hidden debug/test menus; pair with a "Debug"
30
30
  // group so the reveal is self-describing.
31
31
  secret?: string;
32
+ // Non-interactive placeholder: the row still renders (dimmed) but is
33
+ // skipped by keyboard nav, never auto-selected, and `run` never fires
34
+ // on it. Use for "coming soon" teasers. Distinct from `secret`, which
35
+ // HIDES the row; `disabled` SHOWS it as unavailable.
36
+ disabled?: boolean | undefined;
32
37
  }