modern-cmdk 1.5.0 → 1.6.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.
@@ -40,7 +40,10 @@ export declare function CommandEmpty({ ref, children, ...props }: CommandEmptyPr
40
40
  interface CommandGroupProps extends ComponentPropsWithRef<"div"> {
41
41
  /** Heading text displayed above the group */
42
42
  readonly heading?: string;
43
- /** Sort priority (lower = higher in list) */
43
+ /**
44
+ * Orders the keys of `CommandState.groupedIds` (lower first). React renders the groups in the JSX order
45
+ * of the group elements, not by this number.
46
+ */
44
47
  readonly priority?: number;
45
48
  /** Force a specific ID */
46
49
  readonly forceId?: string;
@@ -111,6 +111,14 @@ function useRegisterItem(value, options) {
111
111
  const dataRef = useRef(options?.data);
112
112
  dataRef.current = options?.data;
113
113
  const keywordsDep = options?.keywords?.join("\0");
114
+ const registeredRef = useRef(false);
115
+ useLayoutEffect(() => () => {
116
+ registeredRef.current = false;
117
+ stable.machine.send({
118
+ type: "UNREGISTER_ITEM",
119
+ id
120
+ });
121
+ }, [stable.machine, id]);
114
122
  useLayoutEffect(() => {
115
123
  const item = {
116
124
  id,
@@ -123,15 +131,10 @@ function useRegisterItem(value, options) {
123
131
  data: dataRef.current
124
132
  };
125
133
  stable.machine.send({
126
- type: "REGISTER_ITEM",
134
+ type: registeredRef.current ? "UPDATE_ITEM" : "REGISTER_ITEM",
127
135
  item
128
136
  });
129
- return () => {
130
- stable.machine.send({
131
- type: "UNREGISTER_ITEM",
132
- id
133
- });
134
- };
137
+ registeredRef.current = true;
135
138
  }, [
136
139
  stable.machine,
137
140
  id,
@@ -178,13 +181,14 @@ const HIDDEN_STYLE = { display: "none" };
178
181
  /** Visual grouping container with an optional heading that hides when no items match. */
179
182
  function CommandGroup({ ref, children, heading, priority, forceId, ...props }) {
180
183
  const stateCtx = useStateContext("Command.Group");
184
+ const stable = useStableContext("Command.Group");
181
185
  const headingId = useId();
182
186
  const groupId = useRegisterGroup(heading, {
183
187
  ...priority !== void 0 && { priority },
184
188
  ...forceId !== void 0 && { forceId }
185
189
  });
186
- const groupItems = stateCtx.state.groupedIds.get(groupId);
187
- const hasVisibleItems = groupItems !== void 0 && groupItems.length > 0;
190
+ const registry = stable.machine.getRegistry();
191
+ const hasVisibleItems = stateCtx.state.groupedIds.get(groupId)?.some((id) => registry.getItem(id)?.groupId === groupId) ?? false;
188
192
  const groupContext = useMemo(() => ({
189
193
  groupId,
190
194
  headingId
@@ -1,11 +1,5 @@
1
1
  /* modern-cmdk/react — tokens, item and list transitions; the dialog and overlay transitions are declared but inert until DT-19 */
2
- /* All animations respect prefers-reduced-motion */
3
- /* Spring easing via linear() approximation — perceptually smooth */
4
- /* Scroll-driven animations via animation-timeline: scroll() */
5
- /* WCAG 2.1 AA: forced-colors, prefers-contrast, prefers-reduced-motion */
6
- /* CSS Logical Properties: full RTL/LTR support */
7
- /* CSS Nesting, @layer cascade control, oklch() color space */
8
- /* Container queries for responsive item layout */
2
+ /* Also: scroll-driven animations (gated by @supports), WCAG 2.1 AA forced-colors, prefers-contrast and prefers-reduced-motion, logical properties for RTL/LTR, nesting, @layer, oklch(), container queries */
9
3
  /* Secondary text (group headings, shortcuts, empty state, placeholder) is coloured with
10
4
  --command-muted-color, never faded with opacity: opacity stacks on the consumer's text
11
5
  colour and cannot be checked against it. The default is light-dark(), so it follows the
@@ -191,7 +185,6 @@
191
185
  [data-command-list] {
192
186
  overflow-y: auto;
193
187
  overscroll-behavior: contain;
194
- scroll-timeline: --list-scroll block;
195
188
  will-change: height;
196
189
  transition: height var(--command-duration-list) var(--command-spring-easing);
197
190
  scrollbar-gutter: stable;
@@ -209,6 +202,7 @@
209
202
 
210
203
  [data-command-item] {
211
204
  cursor: pointer;
205
+ -webkit-user-select: none;
212
206
  user-select: none;
213
207
  transition: background-color var(--command-duration-item) linear(0, 0.25, 0.5, 0.75, 1);
214
208
  scroll-margin-block: 8px;
@@ -237,6 +231,7 @@
237
231
  /* ─── Group ─── */
238
232
 
239
233
  [data-command-group-heading] {
234
+ -webkit-user-select: none;
240
235
  user-select: none;
241
236
  font-size: 0.75em;
242
237
  text-transform: uppercase;
@@ -340,9 +335,7 @@
340
335
  overlay var(--command-duration-exit) allow-discrete;
341
336
  }
342
337
 
343
- /* ─── Dialog Animations ─── */
344
- /* Dialog transitions: inert until DT-19 */
345
- /* Spring easing via linear() approximation */
338
+ /* ─── Dialog Animations (inert until DT-19) ─── */
346
339
 
347
340
  [data-command-dialog][data-state="open"] {
348
341
  @starting-style {
@@ -400,24 +393,16 @@
400
393
  &::before {
401
394
  inset-block-start: 0;
402
395
  background: linear-gradient(to bottom, currentColor, transparent);
403
- opacity: 0.08;
404
- animation: fade-edge-top linear both;
405
- animation-timeline: --list-scroll;
406
- animation-range: 0px 24px;
407
396
  }
408
397
 
409
398
  /* Bottom fade — visible when not scrolled to bottom */
410
399
  &::after {
411
400
  inset-block-end: 0;
412
401
  background: linear-gradient(to top, currentColor, transparent);
413
- opacity: 0.08;
414
- animation: fade-edge-bottom linear both;
415
- animation-timeline: --list-scroll;
416
- animation-range: calc(100% - 24px) 100%;
417
402
  }
418
403
  }
419
404
 
420
- @keyframes fade-edge-top {
405
+ @keyframes fade-edge {
421
406
  from {
422
407
  opacity: 0;
423
408
  }
@@ -426,23 +411,12 @@
426
411
  }
427
412
  }
428
413
 
429
- @keyframes fade-edge-bottom {
430
- from {
431
- opacity: 0.08;
432
- }
433
- to {
434
- opacity: 0;
435
- }
436
- }
437
-
438
414
  /* ─── Scroll-Driven Progress Indicator ─── */
439
415
 
440
416
  [data-command-scroll-indicator] {
441
417
  block-size: 2px;
442
418
  transform-origin: inline-start center;
443
419
  scale: 0 1;
444
- animation: scroll-progress linear both;
445
- animation-timeline: --list-scroll;
446
420
  will-change: scale;
447
421
  }
448
422
 
@@ -455,6 +429,34 @@
455
429
  }
456
430
  }
457
431
 
432
+ /* Safari < 26 and Firefox < 159 lack animation-timeline: a time-based run would settle in its end state, so the wiring waits */
433
+ @supports (animation-timeline: scroll()) {
434
+ [data-command-list] {
435
+ scroll-timeline: --list-scroll;
436
+
437
+ &::before {
438
+ animation: fade-edge linear both;
439
+ animation-range: 0px 24px;
440
+ }
441
+
442
+ &::after {
443
+ animation: fade-edge linear both reverse;
444
+ animation-range: calc(100% - 24px) 100%;
445
+ }
446
+
447
+ &::before,
448
+ &::after {
449
+ opacity: 0.08;
450
+ animation-timeline: --list-scroll;
451
+ }
452
+ }
453
+
454
+ [data-command-scroll-indicator] {
455
+ animation: scroll-progress linear both;
456
+ animation-timeline: --list-scroll;
457
+ }
458
+ }
459
+
458
460
  /* ─── Focus Indicators ─── */
459
461
 
460
462
  [data-command-item]:focus-visible {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modern-cmdk",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "type": "module",
5
5
  "description": "The definitive command palette engine for the modern web",
6
6
  "license": "MIT",