@shipfox/react-ui 0.30.0 → 0.31.1

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.
Files changed (27) hide show
  1. package/dist/components/code-block/code-content.js +5 -2
  2. package/dist/components/dashboard/components/kpi-card.js +2 -2
  3. package/dist/components/shipql-editor/index.d.ts +1 -1
  4. package/dist/components/shipql-editor/index.js +1 -1
  5. package/dist/components/shipql-editor/lexical/leaf-close-overlay.js +65 -2
  6. package/dist/components/shipql-editor/lexical/shipql-plugin.js +10 -1
  7. package/dist/components/shipql-editor/shipql-editor-inner.d.ts +1 -1
  8. package/dist/components/shipql-editor/shipql-editor-inner.js +148 -41
  9. package/dist/components/shipql-editor/shipql-editor.d.ts +12 -0
  10. package/dist/components/shipql-editor/shipql-editor.js +1 -1
  11. package/dist/components/shipql-editor/suggestions/generate-suggestions.d.ts +22 -0
  12. package/dist/components/shipql-editor/suggestions/generate-suggestions.js +170 -0
  13. package/dist/components/shipql-editor/suggestions/shipql-range-facet-panel.d.ts +9 -0
  14. package/dist/components/shipql-editor/suggestions/shipql-range-facet-panel.js +376 -0
  15. package/dist/components/shipql-editor/suggestions/shipql-suggestion-item.d.ts +11 -0
  16. package/dist/components/shipql-editor/suggestions/shipql-suggestion-item.js +40 -0
  17. package/dist/components/shipql-editor/suggestions/shipql-suggestions-dropdown.d.ts +19 -0
  18. package/dist/components/shipql-editor/suggestions/shipql-suggestions-dropdown.js +128 -0
  19. package/dist/components/shipql-editor/suggestions/shipql-suggestions-footer.d.ts +11 -0
  20. package/dist/components/shipql-editor/suggestions/shipql-suggestions-footer.js +123 -0
  21. package/dist/components/shipql-editor/suggestions/shipql-suggestions-plugin.d.ts +27 -0
  22. package/dist/components/shipql-editor/suggestions/shipql-suggestions-plugin.js +407 -0
  23. package/dist/components/shipql-editor/suggestions/types.d.ts +20 -0
  24. package/dist/components/shipql-editor/suggestions/types.js +3 -0
  25. package/dist/components/table/data-table.js +2 -2
  26. package/dist/styles.css +1 -1
  27. package/package.json +3 -3
@@ -0,0 +1,123 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Icon } from '../../../components/icon/index.js';
3
+ import { Kbd } from '../../../components/kbd/index.js';
4
+ import { cn } from '../../../utils/cn.js';
5
+ function FooterBadge({ label, kbd }) {
6
+ return /*#__PURE__*/ _jsxs("span", {
7
+ className: cn('flex items-center gap-4 text-xs select-none transition-colors duration-100 text-foreground-neutral-muted'),
8
+ children: [
9
+ label,
10
+ /*#__PURE__*/ _jsx(Kbd, {
11
+ className: "h-16 shrink-0",
12
+ children: kbd
13
+ })
14
+ ]
15
+ });
16
+ }
17
+ function SyntaxHint({ label, example }) {
18
+ return /*#__PURE__*/ _jsxs("span", {
19
+ className: "flex items-center gap-4 text-xs",
20
+ children: [
21
+ /*#__PURE__*/ _jsx("span", {
22
+ className: "text-foreground-neutral-subtle",
23
+ children: label
24
+ }),
25
+ /*#__PURE__*/ _jsx("span", {
26
+ className: "text-tag-purple-icon",
27
+ children: example
28
+ })
29
+ ]
30
+ });
31
+ }
32
+ const SYNTAX_HINTS = {
33
+ value: [
34
+ {
35
+ label: 'Operators',
36
+ example: '(a OR b), a AND b'
37
+ },
38
+ {
39
+ label: 'Negate',
40
+ example: '-a:b, NOT a:b'
41
+ },
42
+ {
43
+ label: 'Wildcard',
44
+ example: '*'
45
+ }
46
+ ],
47
+ range: [
48
+ {
49
+ label: 'Range',
50
+ example: '[min TO max]'
51
+ },
52
+ {
53
+ label: 'Operators',
54
+ example: '>=, <=, >, <, ='
55
+ }
56
+ ]
57
+ };
58
+ export function ShipQLSuggestionsFooter({ showValueActions, showSyntaxHelp, onToggleSyntaxHelp, isError, syntaxHintMode }) {
59
+ const syntaxVisible = showSyntaxHelp || isError;
60
+ const hints = SYNTAX_HINTS[syntaxHintMode];
61
+ return /*#__PURE__*/ _jsxs("div", {
62
+ className: "shrink-0 flex flex-col bg-background-field-base",
63
+ children: [
64
+ syntaxVisible && /*#__PURE__*/ _jsxs(_Fragment, {
65
+ children: [
66
+ /*#__PURE__*/ _jsx("div", {
67
+ className: "border-t border-border-neutral-base-component"
68
+ }),
69
+ isError && /*#__PURE__*/ _jsx("div", {
70
+ className: "bg-background-accent-error-base/8 px-8 py-4",
71
+ children: /*#__PURE__*/ _jsx("p", {
72
+ className: "text-xs font-medium text-foreground-highlight-error",
73
+ children: "Invalid characters in input"
74
+ })
75
+ }),
76
+ /*#__PURE__*/ _jsx("div", {
77
+ className: "flex flex-wrap items-center gap-x-16 gap-y-8 px-8 py-4",
78
+ children: hints.map((hint)=>/*#__PURE__*/ _jsx(SyntaxHint, {
79
+ label: hint.label,
80
+ example: hint.example
81
+ }, hint.label))
82
+ })
83
+ ]
84
+ }),
85
+ /*#__PURE__*/ _jsxs("div", {
86
+ className: "border-t border-border-neutral-base-component flex items-center justify-between gap-12 px-8 py-4",
87
+ children: [
88
+ /*#__PURE__*/ _jsxs("button", {
89
+ onMouseDown: (e)=>{
90
+ e.preventDefault();
91
+ e.stopPropagation();
92
+ onToggleSyntaxHelp();
93
+ },
94
+ className: cn('flex items-center gap-4 px-6 py-2 rounded-6 text-xs font-medium transition-colors cursor-pointer', syntaxVisible ? 'text-foreground-neutral-base hover:bg-background-button-transparent-hover' : 'text-foreground-neutral-muted hover:text-foreground-neutral-subtle hover:bg-background-button-transparent-hover'),
95
+ type: "button",
96
+ children: [
97
+ /*#__PURE__*/ _jsx(Icon, {
98
+ name: "questionLine",
99
+ className: "size-12"
100
+ }),
101
+ "Syntax"
102
+ ]
103
+ }),
104
+ /*#__PURE__*/ _jsxs("div", {
105
+ className: "flex items-center gap-4",
106
+ children: [
107
+ showValueActions && /*#__PURE__*/ _jsx(FooterBadge, {
108
+ label: "Negate",
109
+ kbd: "Shift"
110
+ }),
111
+ /*#__PURE__*/ _jsx(FooterBadge, {
112
+ label: "New tag",
113
+ kbd: "Tab"
114
+ })
115
+ ]
116
+ })
117
+ ]
118
+ })
119
+ ]
120
+ });
121
+ }
122
+
123
+ //# sourceMappingURL=shipql-suggestions-footer.js.map
@@ -0,0 +1,27 @@
1
+ import type { AstNode } from '@shipfox/shipql-parser';
2
+ import type { LeafAstNode } from '../lexical/shipql-leaf-node';
3
+ import type { FacetDef, SuggestionItem } from './types';
4
+ interface ShipQLSuggestionsPluginProps {
5
+ facets: FacetDef[];
6
+ currentFacet: string | null;
7
+ setCurrentFacet: (facet: string | null) => void;
8
+ valueSuggestions: string[];
9
+ isLoadingValueSuggestions: boolean;
10
+ open: boolean;
11
+ setOpen: (open: boolean) => void;
12
+ selectedIndex: number;
13
+ setSelectedIndex: (i: number) => void;
14
+ items: SuggestionItem[];
15
+ setItems: (items: SuggestionItem[]) => void;
16
+ isSelectingRef: React.RefObject<boolean>;
17
+ applyRef: React.RefObject<((value: string) => void) | null>;
18
+ negationPrefixRef: React.RefObject<string>;
19
+ focusedLeafNode: LeafAstNode | null;
20
+ onLeafChange?: (payload: {
21
+ partialValue: string;
22
+ ast: AstNode | null;
23
+ }) => void;
24
+ }
25
+ export declare function ShipQLSuggestionsPlugin({ facets, currentFacet, setCurrentFacet, valueSuggestions, isLoadingValueSuggestions, open, setOpen, selectedIndex, setSelectedIndex, items, setItems, isSelectingRef, applyRef, negationPrefixRef, focusedLeafNode, onLeafChange, }: ShipQLSuggestionsPluginProps): null;
26
+ export {};
27
+ //# sourceMappingURL=shipql-suggestions-plugin.d.ts.map
@@ -0,0 +1,407 @@
1
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
2
+ import { $createRangeSelection, $createTextNode, $getRoot, $getSelection, $isRangeSelection, $isTextNode, $setSelection, BLUR_COMMAND, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_LOW, COMMAND_PRIORITY_NORMAL, FOCUS_COMMAND, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ENTER_COMMAND, KEY_ESCAPE_COMMAND, KEY_TAB_COMMAND } from 'lexical';
3
+ import { useEffect, useRef } from 'react';
4
+ import { $isShipQLLeafNode } from '../lexical/shipql-leaf-node.js';
5
+ import { buildSuggestionItems, detectFacetContext, extractFacetFromLeaf, negationPrefixFromSource, normalizeFacets, stripNegationPrefix, tryParse } from './generate-suggestions.js';
6
+ function getActiveSegment(para) {
7
+ const children = para.getChildren();
8
+ let active = '';
9
+ for(let i = children.length - 1; i >= 0; i--){
10
+ const child = children[i];
11
+ if ($isShipQLLeafNode(child)) {
12
+ // When trailing text starts immediately after a leaf that contains ':',
13
+ // the user is typing a value continuation — e.g.
14
+ // [Leaf("status:"), Text("s")] should yield "status:s", not just "s".
15
+ const leafText = child.getTextContent();
16
+ if (active.length > 0 && active[0] !== ' ' && leafText.includes(':')) {
17
+ active = leafText + active;
18
+ }
19
+ break;
20
+ }
21
+ active = child.getTextContent() + active;
22
+ }
23
+ return active.trimStart();
24
+ }
25
+ export function ShipQLSuggestionsPlugin({ facets, currentFacet, setCurrentFacet, valueSuggestions, isLoadingValueSuggestions, open, setOpen, selectedIndex, setSelectedIndex, items, setItems, isSelectingRef, applyRef, negationPrefixRef, focusedLeafNode, onLeafChange }) {
26
+ const [editor] = useLexicalComposerContext();
27
+ // Stable refs so callbacks always see the latest values without re-registering.
28
+ const openRef = useRef(open);
29
+ openRef.current = open;
30
+ const itemsRef = useRef(items);
31
+ itemsRef.current = items;
32
+ const selectedIndexRef = useRef(selectedIndex);
33
+ selectedIndexRef.current = selectedIndex;
34
+ const setOpenRef = useRef(setOpen);
35
+ setOpenRef.current = setOpen;
36
+ const setSelectedIndexRef = useRef(setSelectedIndex);
37
+ setSelectedIndexRef.current = setSelectedIndex;
38
+ const setItemsRef = useRef(setItems);
39
+ setItemsRef.current = setItems;
40
+ const facetsRef = useRef(facets);
41
+ facetsRef.current = facets;
42
+ const valueSuggestionsRef = useRef(valueSuggestions);
43
+ valueSuggestionsRef.current = valueSuggestions;
44
+ const setCurrentFacetRef = useRef(setCurrentFacet);
45
+ setCurrentFacetRef.current = setCurrentFacet;
46
+ const currentFacetRef = useRef(currentFacet);
47
+ currentFacetRef.current = currentFacet;
48
+ const focusedLeafNodeRef = useRef(focusedLeafNode);
49
+ focusedLeafNodeRef.current = focusedLeafNode;
50
+ const isLoadingValueSuggestionsRef = useRef(isLoadingValueSuggestions);
51
+ isLoadingValueSuggestionsRef.current = isLoadingValueSuggestions;
52
+ const onLeafChangeRef = useRef(onLeafChange);
53
+ onLeafChangeRef.current = onLeafChange;
54
+ const isFocusedRef = useRef(false);
55
+ const hasNavigatedRef = useRef(false);
56
+ const prevFocusedLeafRef = useRef(null);
57
+ // ── Leaf refocus effect ─────────────────────────────────────────────────────
58
+ useEffect(()=>{
59
+ if (focusedLeafNode === prevFocusedLeafRef.current) return;
60
+ prevFocusedLeafRef.current = focusedLeafNode;
61
+ if (focusedLeafNode) {
62
+ if (focusedLeafNode.type === 'text') {
63
+ // Text-type leaves (e.g. "status:") aren't real facet:value nodes.
64
+ // Detect facet context from the text content, mirroring the update
65
+ // listener's handling of text-type leaves.
66
+ const activeText = focusedLeafNode.value;
67
+ const facetCtx = detectFacetContext(activeText, normalizeFacets(facetsRef.current));
68
+ negationPrefixRef.current = facetCtx?.negationPrefix ?? stripNegationPrefix(activeText).prefix;
69
+ setCurrentFacetRef.current(facetCtx?.facet ?? null);
70
+ let text = '';
71
+ editor.getEditorState().read(()=>{
72
+ text = $getRoot().getTextContent();
73
+ });
74
+ onLeafChangeRef.current?.({
75
+ partialValue: facetCtx?.partialValue ?? '',
76
+ ast: tryParse(text)
77
+ });
78
+ const leafItems = buildSuggestionItems(facetsRef.current, valueSuggestionsRef.current, activeText, null);
79
+ setItemsRef.current(leafItems);
80
+ } else {
81
+ const facet = extractFacetFromLeaf(focusedLeafNode);
82
+ negationPrefixRef.current = focusedLeafNode.type === 'not' ? negationPrefixFromSource(focusedLeafNode.source) : '';
83
+ setCurrentFacetRef.current(facet ?? null);
84
+ // Don't call onLeafChange here — the update listener that
85
+ // preceded this rebuild already set the correct partial value.
86
+ const leafItems = buildSuggestionItems(facetsRef.current, valueSuggestionsRef.current, '', focusedLeafNode);
87
+ setItemsRef.current(leafItems);
88
+ }
89
+ hasNavigatedRef.current = false;
90
+ setSelectedIndexRef.current(-1);
91
+ if (isFocusedRef.current) setOpenRef.current(true);
92
+ } else {
93
+ // Leaf deselected — regenerate text-based suggestions immediately
94
+ editor.getEditorState().read(()=>{
95
+ const para = $getRoot().getFirstChild();
96
+ if (!para) return;
97
+ const activeText = getActiveSegment(para);
98
+ const facetCtx = detectFacetContext(activeText, normalizeFacets(facetsRef.current));
99
+ negationPrefixRef.current = facetCtx?.negationPrefix ?? stripNegationPrefix(activeText).prefix;
100
+ setCurrentFacetRef.current(facetCtx?.facet ?? null);
101
+ const text = $getRoot().getTextContent();
102
+ onLeafChangeRef.current?.({
103
+ partialValue: facetCtx?.partialValue ?? '',
104
+ ast: tryParse(text)
105
+ });
106
+ const newItems = buildSuggestionItems(facetsRef.current, valueSuggestionsRef.current, activeText, null);
107
+ setItemsRef.current(newItems);
108
+ hasNavigatedRef.current = false;
109
+ setSelectedIndexRef.current(-1);
110
+ });
111
+ }
112
+ }, [
113
+ focusedLeafNode,
114
+ editor,
115
+ negationPrefixRef
116
+ ]);
117
+ // ── Reactive regeneration when facets/valueSuggestions change ──────────────
118
+ useEffect(()=>{
119
+ if (!openRef.current) return;
120
+ if (prevFocusedLeafRef.current && prevFocusedLeafRef.current.type !== 'text') {
121
+ const leafItems = buildSuggestionItems(facets, valueSuggestions, '', prevFocusedLeafRef.current);
122
+ setItemsRef.current(leafItems);
123
+ setOpenRef.current(isSelectingRef.current || isFocusedRef.current);
124
+ } else if (prevFocusedLeafRef.current?.type === 'text') {
125
+ // Text-type leaf (e.g. "status:") lives inside a ShipQLLeafNode, so
126
+ // getActiveSegment() would return '' — use the leaf's value instead.
127
+ const activeText = prevFocusedLeafRef.current.value;
128
+ const newItems = buildSuggestionItems(facets, valueSuggestions, activeText, null);
129
+ setItemsRef.current(newItems);
130
+ setOpenRef.current(isSelectingRef.current || isFocusedRef.current);
131
+ } else {
132
+ editor.getEditorState().read(()=>{
133
+ const para = $getRoot().getFirstChild();
134
+ if (!para) return;
135
+ const activeText = getActiveSegment(para);
136
+ const newItems = buildSuggestionItems(facets, valueSuggestions, activeText, null);
137
+ setItemsRef.current(newItems);
138
+ hasNavigatedRef.current = false;
139
+ setSelectedIndexRef.current(-1);
140
+ setOpenRef.current(isSelectingRef.current || isFocusedRef.current);
141
+ });
142
+ }
143
+ }, [
144
+ facets,
145
+ valueSuggestions,
146
+ editor,
147
+ isSelectingRef
148
+ ]);
149
+ // ── Apply function ──────────────────────────────────────────────────────────
150
+ useEffect(()=>{
151
+ applyRef.current = (selectedValue)=>{
152
+ const isFacetSelection = !currentFacetRef.current;
153
+ editor.update(()=>{
154
+ const para = $getRoot().getFirstChild();
155
+ if (!para) return;
156
+ const insertText = currentFacetRef.current ? `${negationPrefixRef.current}${currentFacetRef.current}:${selectedValue} ` : `${negationPrefixRef.current}${selectedValue}:`;
157
+ // Case 1: Cursor is inside a focused leaf chip — replace the chip in-place
158
+ if (focusedLeafNodeRef.current) {
159
+ const sel = $getSelection();
160
+ if ($isRangeSelection(sel)) {
161
+ const anchor = sel.anchor.getNode();
162
+ if ($isShipQLLeafNode(anchor)) {
163
+ const newNode = $createTextNode(insertText);
164
+ anchor.insertBefore(newNode);
165
+ anchor.remove();
166
+ const newSel = $createRangeSelection();
167
+ const len = newNode.getTextContentSize();
168
+ newSel.anchor.set(newNode.getKey(), len, 'text');
169
+ newSel.focus.set(newNode.getKey(), len, 'text');
170
+ $setSelection(newSel);
171
+ return;
172
+ }
173
+ }
174
+ }
175
+ // Case 2: Cursor is in a plain text node — replace that node's content
176
+ const sel = $getSelection();
177
+ if ($isRangeSelection(sel)) {
178
+ const anchor = sel.anchor.getNode();
179
+ if ($isTextNode(anchor) && !$isShipQLLeafNode(anchor)) {
180
+ const prevSibling = anchor.getPreviousSibling();
181
+ const prevText = prevSibling?.getTextContent() ?? '';
182
+ const needsSpace = prevText.length > 0 && !prevText.endsWith(' ');
183
+ const finalText = needsSpace ? ` ${insertText}` : insertText;
184
+ anchor.setTextContent(finalText);
185
+ const newSel = $createRangeSelection();
186
+ newSel.anchor.set(anchor.getKey(), finalText.length, 'text');
187
+ newSel.focus.set(anchor.getKey(), finalText.length, 'text');
188
+ $setSelection(newSel);
189
+ return;
190
+ }
191
+ }
192
+ // Case 3: Fallback — append after last leaf chip
193
+ const children = para.getChildren();
194
+ let lastLeafIdx = -1;
195
+ for(let i = children.length - 1; i >= 0; i--){
196
+ if ($isShipQLLeafNode(children[i])) {
197
+ lastLeafIdx = i;
198
+ break;
199
+ }
200
+ }
201
+ for(let i = children.length - 1; i > lastLeafIdx; i--){
202
+ children[i].remove();
203
+ }
204
+ const currentText = para.getTextContent();
205
+ const finalText = currentText.length > 0 && !currentText.endsWith(' ') ? ` ${insertText}` : insertText;
206
+ const newNode = $createTextNode(finalText);
207
+ para.append(newNode);
208
+ const newSel = $createRangeSelection();
209
+ const len = newNode.getTextContentSize();
210
+ newSel.anchor.set(newNode.getKey(), len, 'text');
211
+ newSel.focus.set(newNode.getKey(), len, 'text');
212
+ $setSelection(newSel);
213
+ }, // Tag facet selections so the update listener skips them — we handle
214
+ // state updates below to avoid the intermediate empty-items state.
215
+ isFacetSelection ? {
216
+ tag: 'shipql-apply'
217
+ } : undefined);
218
+ // For facet selections (e.g. "status" → inserts "status:"), notify the
219
+ // parent directly. The update listener is skipped (tagged) because it
220
+ // would call buildSuggestionItems with stale empty valueSuggestions,
221
+ // causing a flash of "No suggestions found".
222
+ if (isFacetSelection) {
223
+ setCurrentFacetRef.current(selectedValue);
224
+ let text = '';
225
+ editor.getEditorState().read(()=>{
226
+ text = $getRoot().getTextContent();
227
+ });
228
+ onLeafChangeRef.current?.({
229
+ partialValue: '',
230
+ ast: tryParse(text)
231
+ });
232
+ setOpenRef.current(true);
233
+ }
234
+ // Defer focus so it fires after the browser's blur event (which hasn't
235
+ // fired yet during the mousedown handler that calls apply). A synchronous
236
+ // focus() call here is a no-op because the editor is still focused.
237
+ setTimeout(()=>editor.focus(), 0);
238
+ hasNavigatedRef.current = false;
239
+ setSelectedIndexRef.current(-1);
240
+ };
241
+ return ()=>{
242
+ applyRef.current = null;
243
+ };
244
+ }, [
245
+ editor,
246
+ applyRef,
247
+ negationPrefixRef
248
+ ]);
249
+ // ── Keyboard + update listeners ─────────────────────────────────────────────
250
+ useEffect(()=>{
251
+ const unregisterFocus = editor.registerCommand(FOCUS_COMMAND, ()=>{
252
+ isFocusedRef.current = true;
253
+ setOpenRef.current(true);
254
+ return false;
255
+ }, COMMAND_PRIORITY_LOW);
256
+ const unregisterBlur = editor.registerCommand(BLUR_COMMAND, ()=>{
257
+ isFocusedRef.current = false;
258
+ if (!isSelectingRef.current) {
259
+ setOpenRef.current(false);
260
+ }
261
+ return false;
262
+ }, COMMAND_PRIORITY_LOW);
263
+ const unregisterArrowDown = editor.registerCommand(KEY_ARROW_DOWN_COMMAND, (e)=>{
264
+ if (!openRef.current || itemsRef.current.length === 0) return false;
265
+ e?.preventDefault();
266
+ const its = itemsRef.current;
267
+ let next = selectedIndexRef.current + 1;
268
+ while(next < its.length && its[next]?.type === 'section-header')next++;
269
+ if (next >= its.length) {
270
+ next = 0;
271
+ while(next < its.length && its[next]?.type === 'section-header')next++;
272
+ }
273
+ if (next < its.length) {
274
+ hasNavigatedRef.current = true;
275
+ setSelectedIndexRef.current(next);
276
+ }
277
+ return true;
278
+ }, COMMAND_PRIORITY_NORMAL);
279
+ const unregisterArrowUp = editor.registerCommand(KEY_ARROW_UP_COMMAND, (e)=>{
280
+ if (!openRef.current || itemsRef.current.length === 0) return false;
281
+ e?.preventDefault();
282
+ const its = itemsRef.current;
283
+ let prev = selectedIndexRef.current - 1;
284
+ while(prev >= 0 && its[prev]?.type === 'section-header')prev--;
285
+ if (prev < 0) {
286
+ prev = its.length - 1;
287
+ while(prev >= 0 && its[prev]?.type === 'section-header')prev--;
288
+ }
289
+ if (prev >= 0) {
290
+ hasNavigatedRef.current = true;
291
+ setSelectedIndexRef.current(prev);
292
+ }
293
+ return true;
294
+ }, COMMAND_PRIORITY_NORMAL);
295
+ const unregisterEnter = editor.registerCommand(KEY_ENTER_COMMAND, (e)=>{
296
+ if (!openRef.current || itemsRef.current.length === 0) return false;
297
+ if (!hasNavigatedRef.current || selectedIndexRef.current < 0) return false;
298
+ const item = itemsRef.current[selectedIndexRef.current];
299
+ if (!item || item.type === 'section-header') return false;
300
+ e?.preventDefault();
301
+ applyRef.current?.(item.value);
302
+ return true;
303
+ }, COMMAND_PRIORITY_CRITICAL);
304
+ const unregisterTab = editor.registerCommand(KEY_TAB_COMMAND, (e)=>{
305
+ if (!openRef.current) return false;
306
+ e?.preventDefault();
307
+ editor.update(()=>{
308
+ const sel = $getSelection();
309
+ if (!$isRangeSelection(sel)) return;
310
+ const anchor = sel.anchor.getNode();
311
+ const para = $getRoot().getFirstChild();
312
+ if (!para) return;
313
+ if ($isShipQLLeafNode(anchor)) {
314
+ const next = anchor.getNextSibling();
315
+ const newSel = $createRangeSelection();
316
+ if (next && $isTextNode(next)) {
317
+ newSel.anchor.set(next.getKey(), 0, 'text');
318
+ newSel.focus.set(next.getKey(), 0, 'text');
319
+ } else {
320
+ const emptyNode = $createTextNode(' ');
321
+ para.append(emptyNode);
322
+ newSel.anchor.set(emptyNode.getKey(), 1, 'text');
323
+ newSel.focus.set(emptyNode.getKey(), 1, 'text');
324
+ }
325
+ $setSelection(newSel);
326
+ return;
327
+ }
328
+ if ($isTextNode(anchor)) {
329
+ const len = anchor.getTextContentSize();
330
+ const newSel = $createRangeSelection();
331
+ newSel.anchor.set(anchor.getKey(), len, 'text');
332
+ newSel.focus.set(anchor.getKey(), len, 'text');
333
+ $setSelection(newSel);
334
+ }
335
+ });
336
+ return true;
337
+ }, COMMAND_PRIORITY_CRITICAL);
338
+ const unregisterEscape = editor.registerCommand(KEY_ESCAPE_COMMAND, (e)=>{
339
+ if (!openRef.current) return false;
340
+ e?.preventDefault();
341
+ setOpenRef.current(false);
342
+ return true;
343
+ }, COMMAND_PRIORITY_NORMAL);
344
+ const unregisterUpdate = editor.registerUpdateListener(({ editorState, tags })=>{
345
+ if (tags.has('shipql-rebuild') || tags.has('shipql-apply')) return;
346
+ editorState.read(()=>{
347
+ const para = $getRoot().getFirstChild();
348
+ if (!para) return;
349
+ let activeText = getActiveSegment(para);
350
+ let focusedLeaf = null;
351
+ const sel = $getSelection();
352
+ if ($isRangeSelection(sel)) {
353
+ const anchor = sel.anchor.getNode();
354
+ if ($isShipQLLeafNode(anchor)) {
355
+ const shipqlNode = anchor.getShipQLNode();
356
+ const textContent = anchor.getTextContent();
357
+ if (shipqlNode.type === 'text' || textContent !== shipqlNode.source) {
358
+ // Text-type leaf, or user is actively typing inside a leaf
359
+ // (text content diverged from the AST source). Use the live
360
+ // text so detectFacetContext can extract the partial value.
361
+ activeText = textContent;
362
+ focusedLeaf = null;
363
+ } else {
364
+ focusedLeaf = shipqlNode;
365
+ }
366
+ }
367
+ }
368
+ const facetCtx = detectFacetContext(activeText, normalizeFacets(facetsRef.current));
369
+ if (focusedLeaf) {
370
+ negationPrefixRef.current = focusedLeaf.type === 'not' ? negationPrefixFromSource(focusedLeaf.source) : '';
371
+ setCurrentFacetRef.current(extractFacetFromLeaf(focusedLeaf) ?? null);
372
+ } else {
373
+ negationPrefixRef.current = facetCtx?.negationPrefix ?? stripNegationPrefix(activeText).prefix;
374
+ setCurrentFacetRef.current(facetCtx?.facet ?? null);
375
+ }
376
+ const text = $getRoot().getTextContent();
377
+ onLeafChangeRef.current?.({
378
+ partialValue: facetCtx?.partialValue ?? '',
379
+ ast: tryParse(text)
380
+ });
381
+ const newItems = buildSuggestionItems(facetsRef.current, valueSuggestionsRef.current, activeText, focusedLeaf);
382
+ setItemsRef.current(newItems);
383
+ hasNavigatedRef.current = false;
384
+ setSelectedIndexRef.current(-1);
385
+ setOpenRef.current(isSelectingRef.current || isFocusedRef.current);
386
+ });
387
+ });
388
+ return ()=>{
389
+ unregisterFocus();
390
+ unregisterBlur();
391
+ unregisterArrowDown();
392
+ unregisterArrowUp();
393
+ unregisterEnter();
394
+ unregisterTab();
395
+ unregisterEscape();
396
+ unregisterUpdate();
397
+ };
398
+ }, [
399
+ editor,
400
+ isSelectingRef,
401
+ applyRef,
402
+ negationPrefixRef
403
+ ]);
404
+ return null;
405
+ }
406
+
407
+ //# sourceMappingURL=shipql-suggestions-plugin.js.map
@@ -0,0 +1,20 @@
1
+ export interface RangeFacetConfig {
2
+ type: 'range';
3
+ min: string;
4
+ max: string;
5
+ presets?: string[];
6
+ }
7
+ export type FacetDef = string | {
8
+ name: string;
9
+ config: RangeFacetConfig;
10
+ };
11
+ export interface SuggestionItem {
12
+ value: string;
13
+ label: React.ReactNode;
14
+ icon: React.ReactNode | null;
15
+ selected: boolean;
16
+ type?: 'section-header' | 'range-slider';
17
+ rangeFacetConfig?: RangeFacetConfig;
18
+ facetName?: string;
19
+ }
20
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,3 @@
1
+ export { };
2
+
3
+ //# sourceMappingURL=types.js.map
@@ -71,7 +71,7 @@ export function DataTable({ columns, data, pagination = true, pageSize = 10, pag
71
71
  getCoreRowModel: getCoreRowModel(),
72
72
  getFilteredRowModel: getFilteredRowModel(),
73
73
  getSortedRowModel: getSortedRowModel(),
74
- getPaginationRowModel: isPaginationNeeded ? getPaginationRowModel() : undefined,
74
+ getPaginationRowModel: pagination ? getPaginationRowModel() : undefined,
75
75
  enableRowSelection: showSelectedCount,
76
76
  onSortingChange: setSorting,
77
77
  onColumnFiltersChange: setColumnFilters,
@@ -83,7 +83,7 @@ export function DataTable({ columns, data, pagination = true, pageSize = 10, pag
83
83
  columnFilters,
84
84
  columnVisibility,
85
85
  rowSelection,
86
- pagination: isPaginationNeeded ? paginationState : undefined
86
+ pagination: pagination ? paginationState : undefined
87
87
  }
88
88
  });
89
89
  const rowModel = table.getRowModel();