cmdk-base
Version:
Fast, unstyled command menu React component.
990 lines (983 loc) • 41.2 kB
JavaScript
'use client';
Object.defineProperty(exports, '__esModule', { value: true });
var React = require('react');
var react = require('@base-ui-components/react');
var useId = require('@base-ui-components/utils/useId');
function _interopNamespace(e) {
if (e && e.__esModule) return e;
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return n;
}
var React__namespace = /*#__PURE__*/_interopNamespace(React);
// This is a fork of https://github.com/pacocoursey/cmdk/blob/main/cmdk/src/command-score.ts
// The scores are arranged so that a continuous match of characters will
// result in a total score of 1.
//
// The best case, this character is a match, and either this is the start
// of the string, or the previous character was also a match.
const SCORE_CONTINUE_MATCH = 1, // A new match at the start of a word scores better than a new match
// elsewhere as it's more likely that the user will type the starts
// of fragments.
// NOTE: We score word jumps between spaces slightly higher than slashes, brackets
// hyphens, etc.
SCORE_SPACE_WORD_JUMP = 0.9, SCORE_NON_SPACE_WORD_JUMP = 0.8, // Any other match isn't ideal, but we include it for completeness.
SCORE_CHARACTER_JUMP = 0.17, // If the user transposed two letters, it should be significantly penalized.
//
// i.e. "ouch" is more likely than "curtain" when "uc" is typed.
SCORE_TRANSPOSITION = 0.1, // The goodness of a match should decay slightly with each missing
// character.
//
// i.e. "bad" is more likely than "bard" when "bd" is typed.
//
// This will not change the order of suggestions based on SCORE_* until
// 100 characters are inserted between matches.
PENALTY_SKIPPED = 0.999, // The goodness of an exact-case match should be higher than a
// case-insensitive match by a small amount.
//
// i.e. "HTML" is more likely than "haml" when "HM" is typed.
//
// This will not change the order of suggestions based on SCORE_* until
// 1000 characters are inserted between matches.
PENALTY_CASE_MISMATCH = 0.9999, // Match higher for letters closer to the beginning of the word
// PENALTY_DISTANCE_FROM_START = 0.9,
// If the word has more characters than the user typed, it should
// be penalised slightly.
//
// i.e. "html" is more likely than "html5" if I type "html".
//
// However, it may well be the case that there's a sensible secondary
// ordering (like alphabetical) that it makes sense to rely on when
// there are many prefix matches, so we don't make the penalty increase
// with the number of tokens.
PENALTY_NOT_COMPLETE = 0.99;
const IS_GAP_REGEXP = /[\\\/_+.#"@\[\(\{&]/, COUNT_GAPS_REGEXP = /[\\\/_+.#"@\[\(\{&]/g, IS_SPACE_REGEXP = /[\s-]/, COUNT_SPACE_REGEXP = /[\s-]/g;
function commandScoreInner(value, abbreviation, lowerString, lowerAbbreviation, stringIndex, abbreviationIndex, memoizedResults) {
if (abbreviationIndex === abbreviation.length) {
if (stringIndex === value.length) {
return SCORE_CONTINUE_MATCH;
}
return PENALTY_NOT_COMPLETE;
}
const memoizeKey = `${stringIndex},${abbreviationIndex}`;
if (memoizedResults[memoizeKey] !== undefined) {
return memoizedResults[memoizeKey];
}
const abbreviationChar = lowerAbbreviation.charAt(abbreviationIndex);
let index = lowerString.indexOf(abbreviationChar, stringIndex);
let highScore = 0;
let score, transposedScore, wordBreaks, spaceBreaks;
while(index >= 0){
score = commandScoreInner(value, abbreviation, lowerString, lowerAbbreviation, index + 1, abbreviationIndex + 1, memoizedResults);
if (score > highScore) {
if (index === stringIndex) {
score *= SCORE_CONTINUE_MATCH;
} else if (IS_GAP_REGEXP.test(value.charAt(index - 1))) {
score *= SCORE_NON_SPACE_WORD_JUMP;
wordBreaks = value.slice(stringIndex, index - 1).match(COUNT_GAPS_REGEXP);
if (wordBreaks && stringIndex > 0) {
score *= Math.pow(PENALTY_SKIPPED, wordBreaks.length);
}
} else if (IS_SPACE_REGEXP.test(value.charAt(index - 1))) {
score *= SCORE_SPACE_WORD_JUMP;
spaceBreaks = value.slice(stringIndex, index - 1).match(COUNT_SPACE_REGEXP);
if (spaceBreaks && stringIndex > 0) {
score *= Math.pow(PENALTY_SKIPPED, spaceBreaks.length);
}
} else {
score *= SCORE_CHARACTER_JUMP;
if (stringIndex > 0) {
score *= Math.pow(PENALTY_SKIPPED, index - stringIndex);
}
}
if (value.charAt(index) !== abbreviation.charAt(abbreviationIndex)) {
score *= PENALTY_CASE_MISMATCH;
}
}
if (score < SCORE_TRANSPOSITION && lowerString.charAt(index - 1) === lowerAbbreviation.charAt(abbreviationIndex + 1) || lowerAbbreviation.charAt(abbreviationIndex + 1) === lowerAbbreviation.charAt(abbreviationIndex) && // allow duplicate letters. Ref #7428
lowerString.charAt(index - 1) !== lowerAbbreviation.charAt(abbreviationIndex)) {
transposedScore = commandScoreInner(value, abbreviation, lowerString, lowerAbbreviation, index + 1, abbreviationIndex + 2, memoizedResults);
if (transposedScore * SCORE_TRANSPOSITION > score) {
score = transposedScore * SCORE_TRANSPOSITION;
}
}
if (score > highScore) {
highScore = score;
}
index = lowerString.indexOf(abbreviationChar, index + 1);
}
memoizedResults[memoizeKey] = highScore;
return highScore;
}
function formatInput(value) {
// convert all valid space characters to space so they match each other
return value.toLowerCase().replace(COUNT_SPACE_REGEXP, " ");
}
function commandScore(string, abbreviation, aliases) {
/* NOTE:
* in the original, we used to do the lower-casing on each recursive call, but this meant that toLowerCase()
* was the dominating cost in the algorithm, passing both is a little ugly, but considerably faster.
*/ string = aliases && aliases.length > 0 ? `${string + " " + aliases.join(" ")}` : string;
return commandScoreInner(string, abbreviation, formatInput(string), formatInput(abbreviation), 0, 0, {});
}
// This is a fork of https://github.com/pacocoursey/cmdk/blob/main/cmdk/src/index.tsx
// @ts-nocheck
/* eslint-disable */
const GROUP_SELECTOR = `[cmdk-group=""]`;
const GROUP_ITEMS_SELECTOR = `[cmdk-group-items=""]`;
const GROUP_HEADING_SELECTOR = `[cmdk-group-heading=""]`;
const ITEM_SELECTOR = `[cmdk-item=""]`;
const VALID_ITEM_SELECTOR = `${ITEM_SELECTOR}:not([aria-disabled="true"])`;
const SELECT_EVENT = `cmdk-item-select`;
const VALUE_ATTR = `data-value`;
const defaultFilter = (value, search, keywords)=>commandScore(value, search, keywords);
const CommandContext = /*#__PURE__*/ React__namespace.createContext(undefined);
const useCommand = ()=>React__namespace.useContext(CommandContext);
const StoreContext = /*#__PURE__*/ React__namespace.createContext(undefined);
const useStore = ()=>React__namespace.useContext(StoreContext);
const GroupContext = /*#__PURE__*/ React__namespace.createContext(undefined);
const Command = /*#__PURE__*/ React__namespace.forwardRef((props, forwardedRef)=>{
const state = useLazyRef(()=>{
var _props_value, _ref;
return {
/** Value of the search query. */ search: "",
/** Currently selected item value. */ value: (_ref = (_props_value = props.value) != null ? _props_value : props.defaultValue) != null ? _ref : "",
/** Currently selected item id. */ selectedItemId: undefined,
filtered: {
/** The count of all visible items. */ count: 0,
/** Map from visible item id to its search score. */ items: new Map(),
/** Set of groups with at least one visible item. */ groups: new Set()
}
};
});
const allItems = useLazyRef(()=>new Set()) // [...itemIds]
;
const allGroups = useLazyRef(()=>new Map()) // groupId → [...itemIds]
;
const ids = useLazyRef(()=>new Map()) // id → { value, keywords }
;
const listeners = useLazyRef(()=>new Set()) // [...rerenders]
;
const propsRef = useAsRef(props);
const { label, children, value, onValueChange, filter, shouldFilter, loop, disablePointerSelection = false, vimBindings = true, ...etc } = props;
const listId = useId.useId();
const labelId = useId.useId();
const inputId = useId.useId();
const listInnerRef = React__namespace.useRef(null);
const schedule = useScheduleLayoutEffect();
/** Controlled mode `value` handling. */ useLayoutEffect(()=>{
if (value !== undefined) {
const v = value.trim();
state.current.value = v;
store.emit();
}
}, [
value
]);
useLayoutEffect(()=>{
schedule(6, scrollSelectedIntoView);
}, []);
const store = React__namespace.useMemo(()=>{
return {
subscribe: (cb)=>{
listeners.current.add(cb);
return ()=>listeners.current.delete(cb);
},
snapshot: ()=>{
return state.current;
},
setState: (key, value, opts)=>{
if (Object.is(state.current[key], value)) return;
state.current[key] = value;
if (key === "search") {
// Filter synchronously before emitting back to children
filterItems();
sort();
schedule(1, selectFirstItem);
} else if (key === "value") {
var _propsRef_current;
// Force focus input or root so accessibility works
if (document.activeElement.hasAttribute("cmdk-input") || document.activeElement.hasAttribute("cmdk-root")) {
var _document_getElementById;
const input = document.getElementById(inputId);
if (input) input.focus();
else (_document_getElementById = document.getElementById(listId)) == null ? undefined : _document_getElementById.focus();
}
schedule(7, ()=>{
var _getSelectedItem;
state.current.selectedItemId = (_getSelectedItem = getSelectedItem()) == null ? undefined : _getSelectedItem.id;
store.emit();
});
// opts is a boolean referring to whether it should NOT be scrolled into view
if (!opts) {
// Scroll the selected item into view
schedule(5, scrollSelectedIntoView);
}
if (((_propsRef_current = propsRef.current) == null ? undefined : _propsRef_current.value) !== undefined) {
// If controlled, just call the callback instead of updating state internally
const newValue = value != null ? value : "";
propsRef.current.onValueChange == null ? undefined : propsRef.current.onValueChange.call(propsRef.current, newValue);
return;
}
}
// Notify subscribers that state has changed
store.emit();
},
emit: ()=>{
listeners.current.forEach((l)=>l());
}
};
}, []);
const context = React__namespace.useMemo(()=>({
// Keep id → {value, keywords} mapping up-to-date
value: (id, value, keywords)=>{
var _ids_current_get;
if (value !== ((_ids_current_get = ids.current.get(id)) == null ? undefined : _ids_current_get.value)) {
ids.current.set(id, {
value,
keywords
});
state.current.filtered.items.set(id, score(value, keywords));
schedule(2, ()=>{
sort();
store.emit();
});
}
},
// Track item lifecycle (mount, unmount)
item: (id, groupId)=>{
allItems.current.add(id);
// Track this item within the group
if (groupId) {
if (!allGroups.current.has(groupId)) {
allGroups.current.set(groupId, new Set([
id
]));
} else {
allGroups.current.get(groupId).add(id);
}
}
// Batch this, multiple items can mount in one pass
// and we should not be filtering/sorting/emitting each time
schedule(3, ()=>{
filterItems();
sort();
// Could be initial mount, select the first item if none already selected
if (!state.current.value) {
selectFirstItem();
}
store.emit();
});
return ()=>{
ids.current.delete(id);
allItems.current.delete(id);
state.current.filtered.items.delete(id);
const selectedItem = getSelectedItem();
// Batch this, multiple items could be removed in one pass
schedule(4, ()=>{
filterItems();
// The item removed have been the selected one,
// so selection should be moved to the first
if ((selectedItem == null ? undefined : selectedItem.getAttribute("id")) === id) selectFirstItem();
store.emit();
});
};
},
// Track group lifecycle (mount, unmount)
group: (id)=>{
if (!allGroups.current.has(id)) {
allGroups.current.set(id, new Set());
}
return ()=>{
ids.current.delete(id);
allGroups.current.delete(id);
};
},
filter: ()=>{
return propsRef.current.shouldFilter;
},
label: label || props["aria-label"],
getDisablePointerSelection: ()=>{
return propsRef.current.disablePointerSelection;
},
listId,
inputId,
labelId,
listInnerRef
}), []);
function score(value, keywords) {
var _propsRef_current;
var _propsRef_current_filter;
const filter = (_propsRef_current_filter = (_propsRef_current = propsRef.current) == null ? undefined : _propsRef_current.filter) != null ? _propsRef_current_filter : defaultFilter;
return value ? filter(value, state.current.search, keywords) : 0;
}
/** Sorts items by score, and groups by highest item score. */ function sort() {
if (!state.current.search || // Explicitly false, because true | undefined is the default
propsRef.current.shouldFilter === false) {
return;
}
const scores = state.current.filtered.items;
// Sort the groups
const groups = [];
state.current.filtered.groups.forEach((value)=>{
const items = allGroups.current.get(value);
// Get the maximum score of the group's items
let max = 0;
items.forEach((item)=>{
const score = scores.get(item);
max = Math.max(score, max);
});
groups.push([
value,
max
]);
});
// Sort items within groups to bottom
// Sort items outside of groups
// Sort groups to bottom (pushes all non-grouped items to the top)
const listInsertionElement = listInnerRef.current;
// Sort the items
getValidItems().sort((a, b)=>{
const valueA = a.getAttribute("id");
const valueB = b.getAttribute("id");
var _scores_get, _scores_get1;
return ((_scores_get = scores.get(valueB)) != null ? _scores_get : 0) - ((_scores_get1 = scores.get(valueA)) != null ? _scores_get1 : 0);
}).forEach((item)=>{
const group = item.closest(GROUP_ITEMS_SELECTOR);
if (group) {
group.appendChild(item.parentElement === group ? item : item.closest(`${GROUP_ITEMS_SELECTOR} > *`));
} else {
listInsertionElement.appendChild(item.parentElement === listInsertionElement ? item : item.closest(`${GROUP_ITEMS_SELECTOR} > *`));
}
});
groups.sort((a, b)=>b[1] - a[1]).forEach((group)=>{
var _listInnerRef_current;
const element = (_listInnerRef_current = listInnerRef.current) == null ? undefined : _listInnerRef_current.querySelector(`${GROUP_SELECTOR}[${VALUE_ATTR}="${encodeURIComponent(group[0])}"]`);
element == null ? undefined : element.parentElement.appendChild(element);
});
}
function selectFirstItem() {
const item = getValidItems().find((item)=>item.getAttribute("aria-disabled") !== "true");
const value = item == null ? undefined : item.getAttribute(VALUE_ATTR);
store.setState("value", value || undefined);
}
/** Filters the current items. */ function filterItems() {
if (!state.current.search || // Explicitly false, because true | undefined is the default
propsRef.current.shouldFilter === false) {
state.current.filtered.count = allItems.current.size;
// Do nothing, each item will know to show itself because search is empty
return;
}
// Reset the groups
state.current.filtered.groups = new Set();
let itemCount = 0;
// Check which items should be included
for (const id of allItems.current){
var _ids_current_get, _ids_current_get1;
var _ids_current_get_value;
const value = (_ids_current_get_value = (_ids_current_get = ids.current.get(id)) == null ? undefined : _ids_current_get.value) != null ? _ids_current_get_value : "";
var _ids_current_get_keywords;
const keywords = (_ids_current_get_keywords = (_ids_current_get1 = ids.current.get(id)) == null ? undefined : _ids_current_get1.keywords) != null ? _ids_current_get_keywords : [];
const rank = score(value, keywords);
state.current.filtered.items.set(id, rank);
if (rank > 0) itemCount++;
}
// Check which groups have at least 1 item shown
for (const [groupId, group] of allGroups.current){
for (const itemId of group){
if (state.current.filtered.items.get(itemId) > 0) {
state.current.filtered.groups.add(groupId);
break;
}
}
}
state.current.filtered.count = itemCount;
}
function scrollSelectedIntoView() {
// Wait for popover positioning to complete before scrolling
requestAnimationFrame(()=>{
const item = getSelectedItem();
if (item) {
var _item_parentElement;
if (((_item_parentElement = item.parentElement) == null ? undefined : _item_parentElement.firstChild) === item) {
var // First item in Group, ensure heading is in view
_item_closest_querySelector, _item_closest;
(_item_closest = item.closest(GROUP_SELECTOR)) == null ? undefined : (_item_closest_querySelector = _item_closest.querySelector(GROUP_HEADING_SELECTOR)) == null ? undefined : _item_closest_querySelector.scrollIntoView({
block: "nearest"
});
}
// Ensure the item is always in view
item.scrollIntoView({
block: "nearest"
});
}
});
}
/** Getters */ function getSelectedItem() {
var _listInnerRef_current;
return (_listInnerRef_current = listInnerRef.current) == null ? undefined : _listInnerRef_current.querySelector(`${ITEM_SELECTOR}[aria-selected="true"]`);
}
function getValidItems() {
var _listInnerRef_current;
return Array.from(((_listInnerRef_current = listInnerRef.current) == null ? undefined : _listInnerRef_current.querySelectorAll(VALID_ITEM_SELECTOR)) || []);
}
/** Setters */ function updateSelectedToIndex(index) {
const items = getValidItems();
const item = items[index];
if (item) store.setState("value", item.getAttribute(VALUE_ATTR));
}
function updateSelectedByItem(change) {
var _propsRef_current;
const selected = getSelectedItem();
const items = getValidItems();
const index = items.findIndex((item)=>item === selected);
// Get item at this index
let newSelected = items[index + change];
if ((_propsRef_current = propsRef.current) == null ? undefined : _propsRef_current.loop) {
newSelected = index + change < 0 ? items[items.length - 1] : index + change === items.length ? items[0] : items[index + change];
}
if (newSelected) store.setState("value", newSelected.getAttribute(VALUE_ATTR));
}
function updateSelectedByGroup(change) {
const selected = getSelectedItem();
let group = selected == null ? undefined : selected.closest(GROUP_SELECTOR);
let item;
while(group && !item){
group = change > 0 ? findNextSibling(group, GROUP_SELECTOR) : findPreviousSibling(group, GROUP_SELECTOR);
item = group == null ? undefined : group.querySelector(VALID_ITEM_SELECTOR);
}
if (item) {
store.setState("value", item.getAttribute(VALUE_ATTR));
} else {
updateSelectedByItem(change);
}
}
const last = ()=>updateSelectedToIndex(getValidItems().length - 1);
const next = (e)=>{
e.preventDefault();
if (e.metaKey) {
// Last item
last();
} else if (e.altKey) {
// Next group
updateSelectedByGroup(1);
} else {
// Next item
updateSelectedByItem(1);
}
};
const prev = (e)=>{
e.preventDefault();
if (e.metaKey) {
// First item
updateSelectedToIndex(0);
} else if (e.altKey) {
// Previous group
updateSelectedByGroup(-1);
} else {
// Previous item
updateSelectedByItem(-1);
}
};
return /*#__PURE__*/ React__namespace.createElement("div", {
ref: forwardedRef,
tabIndex: -1,
...etc,
"cmdk-root": "",
onKeyDown: (e)=>{
etc.onKeyDown == null ? undefined : etc.onKeyDown.call(etc, e);
// Check if IME composition is finished before triggering key binds
// This prevents unwanted triggering while user is still inputting text with IME
// e.keyCode === 229 is for the CJK IME with Legacy Browser [https://w3c.github.io/uievents/#determine-keydown-keyup-keyCode]
// isComposing is for the CJK IME with Modern Browser [https://developer.mozilla.org/en-US/docs/Web/API/CompositionEvent/isComposing]
const isComposing = e.nativeEvent.isComposing || e.keyCode === 229;
if (e.defaultPrevented || isComposing) {
return;
}
switch(e.key){
case "n":
case "j":
{
// vim keybind down
if (vimBindings && e.ctrlKey) {
next(e);
}
break;
}
case "ArrowDown":
{
next(e);
break;
}
case "p":
case "k":
{
// vim keybind up
if (vimBindings && e.ctrlKey) {
prev(e);
}
break;
}
case "ArrowUp":
{
prev(e);
break;
}
case "Home":
{
// First item
e.preventDefault();
updateSelectedToIndex(0);
break;
}
case "End":
{
// Last item
e.preventDefault();
last();
break;
}
case "Enter":
{
// Trigger item onSelect
e.preventDefault();
const item = getSelectedItem();
if (item) {
const event = new Event(SELECT_EVENT);
item.dispatchEvent(event);
}
}
}
}
}, /*#__PURE__*/ React__namespace.createElement("label", {
"cmdk-label": "",
htmlFor: context.inputId,
id: context.labelId,
// Screen reader only
style: srOnlyStyles
}, label), SlottableWithNestedChildren(props, (child)=>/*#__PURE__*/ React__namespace.createElement(StoreContext.Provider, {
value: store
}, /*#__PURE__*/ React__namespace.createElement(CommandContext.Provider, {
value: context
}, child))));
});
/**
* Command menu item. Becomes active on pointer enter or through keyboard navigation.
* Preferably pass a `value`, otherwise the value will be inferred from `children` or
* the rendered item's `textContent`.
*/ const Item = /*#__PURE__*/ React__namespace.forwardRef((props, forwardedRef)=>{
var _propsRef_current;
const id = useId.useId();
const ref = React__namespace.useRef(null);
const groupContext = React__namespace.useContext(GroupContext);
const context = useCommand();
const propsRef = useAsRef(props);
var _propsRef_current_forceMount;
const forceMount = (_propsRef_current_forceMount = (_propsRef_current = propsRef.current) == null ? undefined : _propsRef_current.forceMount) != null ? _propsRef_current_forceMount : groupContext == null ? undefined : groupContext.forceMount;
useLayoutEffect(()=>{
if (!forceMount) {
return context.item(id, groupContext == null ? undefined : groupContext.id);
}
}, [
forceMount
]);
const value = useValue(id, ref, [
props.value,
props.children,
ref
], props.keywords);
const store = useStore();
const selected = useCmdk((state)=>state.value && state.value === value.current);
const render = useCmdk((state)=>forceMount ? true : context.filter() === false ? true : !state.search ? true : state.filtered.items.get(id) > 0);
React__namespace.useEffect(()=>{
const element = ref.current;
if (!element || props.disabled) return;
element.addEventListener(SELECT_EVENT, onSelect);
return ()=>element.removeEventListener(SELECT_EVENT, onSelect);
}, [
render,
props.onSelect,
props.disabled
]);
function onSelect() {
select();
propsRef.current.onSelect == null ? undefined : propsRef.current.onSelect.call(propsRef.current, value.current);
}
function select() {
store.setState("value", value.current, true);
}
if (!render) return null;
const { disabled, value: _, onSelect: __, forceMount: ___, keywords: ____, ...etc } = props;
return /*#__PURE__*/ React__namespace.createElement("div", {
ref: mergeRefs([
ref,
forwardedRef
]),
...etc,
id: id,
"cmdk-item": "",
role: "option",
"aria-disabled": Boolean(disabled),
"aria-selected": Boolean(selected),
"data-disabled": Boolean(disabled),
"data-selected": Boolean(selected),
onPointerMove: disabled || context.getDisablePointerSelection() ? undefined : select,
onClick: disabled ? undefined : onSelect
}, props.children);
});
/**
* Group command menu items together with a heading.
* Grouped items are always shown together.
*/ const Group = /*#__PURE__*/ React__namespace.forwardRef((props, forwardedRef)=>{
const { heading, children, forceMount, ...etc } = props;
const id = useId.useId();
const ref = React__namespace.useRef(null);
const headingRef = React__namespace.useRef(null);
const headingId = useId.useId();
const context = useCommand();
const render = useCmdk((state)=>forceMount ? true : context.filter() === false ? true : !state.search ? true : state.filtered.groups.has(id));
useLayoutEffect(()=>{
return context.group(id);
}, []);
useValue(id, ref, [
props.value,
props.heading,
headingRef
]);
const contextValue = React__namespace.useMemo(()=>({
id,
forceMount
}), [
forceMount
]);
return /*#__PURE__*/ React__namespace.createElement("div", {
ref: mergeRefs([
ref,
forwardedRef
]),
...etc,
"cmdk-group": "",
role: "presentation",
hidden: render ? undefined : true
}, heading && /*#__PURE__*/ React__namespace.createElement("div", {
ref: headingRef,
"cmdk-group-heading": "",
"aria-hidden": true,
id: headingId
}, heading), SlottableWithNestedChildren(props, (child)=>/*#__PURE__*/ React__namespace.createElement("div", {
"cmdk-group-items": "",
role: "group",
"aria-labelledby": heading ? headingId : undefined
}, /*#__PURE__*/ React__namespace.createElement(GroupContext.Provider, {
value: contextValue
}, child))));
});
/**
* A visual and semantic separator between items or groups.
* Visible when the search query is empty or `alwaysRender` is true, hidden otherwise.
*/ const Separator = /*#__PURE__*/ React__namespace.forwardRef((props, forwardedRef)=>{
const { alwaysRender, ...etc } = props;
const ref = React__namespace.useRef(null);
const render = useCmdk((state)=>!state.search);
if (!alwaysRender && !render) return null;
return /*#__PURE__*/ React__namespace.createElement("div", {
ref: mergeRefs([
ref,
forwardedRef
]),
...etc,
"cmdk-separator": "",
role: "separator"
});
});
/**
* Command menu input.
* All props are forwarded to the underyling `input` element.
*/ const Input = /*#__PURE__*/ React__namespace.forwardRef((props, forwardedRef)=>{
const { onValueChange, ...etc } = props;
const isControlled = props.value != null;
const store = useStore();
const search = useCmdk((state)=>state.search);
const selectedItemId = useCmdk((state)=>state.selectedItemId);
const context = useCommand();
React__namespace.useEffect(()=>{
if (props.value != null) {
store.setState("search", props.value);
}
}, [
props.value
]);
return /*#__PURE__*/ React__namespace.createElement("input", {
ref: forwardedRef,
...etc,
"cmdk-input": "",
autoComplete: "off",
autoCorrect: "off",
spellCheck: false,
"aria-autocomplete": "list",
role: "combobox",
"aria-expanded": true,
"aria-controls": context.listId,
"aria-labelledby": context.labelId,
"aria-activedescendant": selectedItemId,
id: context.inputId,
type: "text",
value: isControlled ? props.value : search,
onChange: (e)=>{
if (!isControlled) {
store.setState("search", e.target.value);
}
onValueChange == null ? undefined : onValueChange(e.target.value);
}
});
});
/**
* Contains `Item`, `Group`, and `Separator`.
* Use the `--cmdk-list-height` CSS variable to animate height based on the number of results.
*/ const List = /*#__PURE__*/ React__namespace.forwardRef((props, forwardedRef)=>{
const { children, label = "Suggestions", ...etc } = props;
const ref = React__namespace.useRef(null);
const height = React__namespace.useRef(null);
const selectedItemId = useCmdk((state)=>state.selectedItemId);
const context = useCommand();
React__namespace.useEffect(()=>{
if (height.current && ref.current) {
const el = height.current;
const wrapper = ref.current;
let animationFrame;
const observer = new ResizeObserver(()=>{
animationFrame = requestAnimationFrame(()=>{
const height = el.offsetHeight;
wrapper.style.setProperty(`--cmdk-list-height`, height.toFixed(1) + "px");
});
});
observer.observe(el);
return ()=>{
cancelAnimationFrame(animationFrame);
observer.unobserve(el);
};
}
}, []);
return /*#__PURE__*/ React__namespace.createElement("div", {
ref: mergeRefs([
ref,
forwardedRef
]),
...etc,
"cmdk-list": "",
role: "listbox",
tabIndex: -1,
"aria-activedescendant": selectedItemId,
"aria-label": label,
id: context.listId
}, SlottableWithNestedChildren(props, (child)=>/*#__PURE__*/ React__namespace.createElement("div", {
ref: mergeRefs([
height,
context.listInnerRef
]),
"cmdk-list-sizer": ""
}, child)));
});
/**
* Renders the command menu in a Base UI Dialog.
*/ const Dialog = /*#__PURE__*/ React__namespace.forwardRef((props, forwardedRef)=>{
const { open, onOpenChange, overlayClassName, contentClassName, container, ...etc } = props;
return /*#__PURE__*/ React__namespace.createElement(react.Dialog.Root, {
open: open,
onOpenChange: onOpenChange
}, /*#__PURE__*/ React__namespace.createElement(react.Dialog.Portal, {
container: container
}, /*#__PURE__*/ React__namespace.createElement(react.Dialog.Backdrop, {
"cmdk-overlay": "",
className: overlayClassName
}), /*#__PURE__*/ React__namespace.createElement(react.Dialog.Popup, {
"aria-label": props.label,
"cmdk-dialog": "",
className: contentClassName
}, /*#__PURE__*/ React__namespace.createElement(Command, {
ref: forwardedRef,
...etc
}))));
});
/**
* Automatically renders when there are no results for the search query.
*/ const Empty = /*#__PURE__*/ React__namespace.forwardRef((props, forwardedRef)=>{
const render = useCmdk((state)=>state.filtered.count === 0);
if (!render) return null;
return /*#__PURE__*/ React__namespace.createElement("div", {
ref: forwardedRef,
...props,
"cmdk-empty": "",
role: "presentation"
});
});
/**
* You should conditionally render this with `progress` while loading asynchronous items.
*/ const Loading = /*#__PURE__*/ React__namespace.forwardRef((props, forwardedRef)=>{
const { progress, children, label = "Loading...", ...etc } = props;
return /*#__PURE__*/ React__namespace.createElement("div", {
ref: forwardedRef,
...etc,
"cmdk-loading": "",
role: "progressbar",
"aria-valuenow": progress,
"aria-valuemin": 0,
"aria-valuemax": 100,
"aria-label": label
}, SlottableWithNestedChildren(props, (child)=>/*#__PURE__*/ React__namespace.createElement("div", {
"aria-hidden": true
}, child)));
});
const pkg = Object.assign(Command, {
List,
Item,
Input,
Group,
Separator,
Dialog,
Empty,
Loading
});
/**
*
*
* Helpers
*
*
*/ function findNextSibling(el, selector) {
let sibling = el.nextElementSibling;
while(sibling){
if (sibling.matches(selector)) return sibling;
sibling = sibling.nextElementSibling;
}
}
function findPreviousSibling(el, selector) {
let sibling = el.previousElementSibling;
while(sibling){
if (sibling.matches(selector)) return sibling;
sibling = sibling.previousElementSibling;
}
}
function useAsRef(data) {
const ref = React__namespace.useRef(data);
useLayoutEffect(()=>{
ref.current = data;
});
return ref;
}
const useLayoutEffect = typeof window === "undefined" ? React__namespace.useEffect : React__namespace.useLayoutEffect;
function useLazyRef(fn) {
const ref = React__namespace.useRef();
if (ref.current === undefined) {
ref.current = fn();
}
return ref;
}
/** Run a selector against the store state. */ function useCmdk(selector) {
const store = useStore();
const cb = ()=>selector(store.snapshot());
return React__namespace.useSyncExternalStore(store.subscribe, cb, cb);
}
function useValue(id, ref, deps, aliases = []) {
const valueRef = React__namespace.useRef();
const context = useCommand();
useLayoutEffect(()=>{
var _ref_current;
const value = (()=>{
for (const part of deps){
if (typeof part === "string") {
return part.trim();
}
if (typeof part === "object" && "current" in part) {
if (part.current) {
var _part_current_textContent;
return (_part_current_textContent = part.current.textContent) == null ? undefined : _part_current_textContent.trim();
}
return valueRef.current;
}
}
})();
const keywords = aliases.map((alias)=>alias.trim());
context.value(id, value, keywords);
(_ref_current = ref.current) == null ? undefined : _ref_current.setAttribute(VALUE_ATTR, value);
valueRef.current = value;
});
return valueRef;
}
/** Imperatively run a function on the next layout effect cycle. */ const useScheduleLayoutEffect = ()=>{
const [s, ss] = React__namespace.useState();
const fns = useLazyRef(()=>new Map());
useLayoutEffect(()=>{
fns.current.forEach((f)=>f());
fns.current = new Map();
}, [
s
]);
return (id, cb)=>{
fns.current.set(id, cb);
ss({});
};
};
function renderChildren(children) {
const childrenType = children.type;
// The children is a component
if (typeof childrenType === "function") return childrenType(children.props);
else if ("render" in childrenType) return childrenType.render(children.props);
else return children;
}
function SlottableWithNestedChildren({ asChild, children }, render) {
if (asChild && /*#__PURE__*/ React__namespace.isValidElement(children)) {
return /*#__PURE__*/ React__namespace.cloneElement(renderChildren(children), {
ref: children.ref
}, render(children.props.children));
}
return render(children);
}
// ESM is still a nightmare with Next.js so I'm just gonna copy the package code in
// https://github.com/gregberge/react-merge-refs
// Copyright (c) 2020 Greg Bergé
function mergeRefs(refs) {
return (value)=>{
refs.forEach((ref)=>{
if (typeof ref === "function") {
ref(value);
} else if (ref != null) {
ref.current = value;
}
});
};
}
const srOnlyStyles = {
position: "absolute",
width: "1px",
height: "1px",
padding: "0",
margin: "-1px",
overflow: "hidden",
clip: "rect(0, 0, 0, 0)",
whiteSpace: "nowrap",
borderWidth: "0"
};
exports.Command = pkg;
exports.CommandDialog = Dialog;
exports.CommandEmpty = Empty;
exports.CommandGroup = Group;
exports.CommandInput = Input;
exports.CommandItem = Item;
exports.CommandList = List;
exports.CommandLoading = Loading;
exports.CommandRoot = Command;
exports.CommandSeparator = Separator;
exports.defaultFilter = defaultFilter;
exports.useCommandState = useCmdk;