@shipfox/react-ui 0.29.0 → 0.31.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.
Files changed (26) 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/styles.css +1 -1
  26. package/package.json +7 -4
@@ -18,10 +18,13 @@ export function CodeContent({ code, highlightedCode, isLoading, syntaxHighlighti
18
18
  ...props,
19
19
  children: /*#__PURE__*/ _jsx("code", {
20
20
  className: cn('w-full overflow-x-auto bg-transparent font-code text-xs leading-20 text-foreground-neutral-base', 'grid', lineNumbers && '[counter-reset:line] [counter-increment:line_0] [&_.line]:before:content-[counter(line)] [&_.line]:before:inline-block [&_.line]:before:[counter-increment:line] [&_.line]:before:w-16 [&_.line]:before:mr-16 [&_.line]:before:text-xs [&_.line]:before:text-right [&_.line]:before:text-foreground-neutral-subtle [&_.line]:before:font-code [&_.line]:before:select-none'),
21
- children: lines.map((line, index)=>/*#__PURE__*/ _jsx("span", {
21
+ children: lines.map((line, index)=>{
22
+ const key = `${index}-${line}`;
23
+ return /*#__PURE__*/ _jsx("span", {
22
24
  className: "line px-12 w-full relative font-code text-xs leading-20",
23
25
  children: line
24
- }, `${index}-${line}`))
26
+ }, key);
27
+ })
25
28
  })
26
29
  });
27
30
  }
@@ -48,12 +48,12 @@ export function KpiCardsGroup({ cards, className, ...props }) {
48
48
  ...props,
49
49
  children: /*#__PURE__*/ _jsx("div", {
50
50
  className: "flex gap-16 pl-0 w-full",
51
- children: cards.map((card, index)=>{
51
+ children: cards.map((card)=>{
52
52
  const { key: _key, ...cardProps } = card;
53
53
  return /*#__PURE__*/ _jsx(KpiCard, {
54
54
  ...cardProps,
55
55
  className: cn('shrink-0 w-[calc((100vw-56px)/2)] snap-start md:flex-1 md:w-0', card.className)
56
- }, `${card.label}-${index}`);
56
+ }, card.label);
57
57
  })
58
58
  })
59
59
  });
@@ -1,3 +1,3 @@
1
1
  export type { LeafAstNode } from './lexical/shipql-leaf-node';
2
- export * from './shipql-editor';
2
+ export { type FacetDef, type LeafChangePayload, type RangeFacetConfig, ShipQLEditor, type ShipQLEditorProps, } from './shipql-editor';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1,3 +1,3 @@
1
- export * from './shipql-editor.js';
1
+ export { ShipQLEditor } from './shipql-editor.js';
2
2
 
3
3
  //# sourceMappingURL=index.js.map
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
3
+ import { Button } from '../../../components/button/index.js';
3
4
  import { useCallback, useEffect, useRef, useState } from 'react';
4
5
  import { cn } from '../../../utils/cn.js';
5
- import { Button } from '../../button/button.js';
6
6
  import { REMOVE_LEAF_COMMAND } from './shipql-plugin.js';
7
7
  const LEAF_ACTIVE_CLASSES = 'ring-1 ring-border-highlights-interactive rounded-r-none';
8
8
  export function LeafCloseOverlay() {
@@ -14,6 +14,7 @@ export function LeafCloseOverlay() {
14
14
  left: 0
15
15
  });
16
16
  const activeLeafElRef = useRef(null);
17
+ const keyboardNavigatingRef = useRef(false);
17
18
  const activateLeaf = useCallback((el)=>{
18
19
  if (activeLeafElRef.current && activeLeafElRef.current !== el) {
19
20
  for (const cls of LEAF_ACTIVE_CLASSES.split(' ')){
@@ -38,6 +39,7 @@ export function LeafCloseOverlay() {
38
39
  }, []);
39
40
  const handleMouseOver = useCallback((e)=>{
40
41
  if (containerRef.current?.contains(e.target)) return;
42
+ if (keyboardNavigatingRef.current) return;
41
43
  const target = e.target.closest('[data-shipql-leaf]');
42
44
  if (target) {
43
45
  const key = target.getAttribute('data-shipql-key');
@@ -71,16 +73,77 @@ export function LeafCloseOverlay() {
71
73
  if (!rootElement) return;
72
74
  const container = rootElement.closest('[data-shipql-editor]');
73
75
  const target = container ?? rootElement;
76
+ const NAV_KEYS = new Set([
77
+ 'ArrowUp',
78
+ 'ArrowDown',
79
+ 'ArrowLeft',
80
+ 'ArrowRight',
81
+ 'Home',
82
+ 'End'
83
+ ]);
84
+ const setLeafPointerEvents = (enabled)=>{
85
+ const leaves = Array.from(target.querySelectorAll('[data-shipql-leaf]'));
86
+ for (const leaf of leaves){
87
+ leaf.style.pointerEvents = enabled ? '' : 'none';
88
+ }
89
+ };
90
+ const handleKeyDown = (e)=>{
91
+ if (NAV_KEYS.has(e.key)) {
92
+ keyboardNavigatingRef.current = true;
93
+ activateLeaf(null);
94
+ setHovered(null);
95
+ setLeafPointerEvents(false);
96
+ }
97
+ };
98
+ const handleMouseMove = ()=>{
99
+ if (!keyboardNavigatingRef.current) return;
100
+ keyboardNavigatingRef.current = false;
101
+ setLeafPointerEvents(true);
102
+ };
74
103
  target.addEventListener('mouseover', handleMouseOver);
75
104
  target.addEventListener('mouseleave', handleMouseLeave);
105
+ target.addEventListener('keydown', handleKeyDown);
106
+ target.addEventListener('mousemove', handleMouseMove);
76
107
  return ()=>{
77
108
  target.removeEventListener('mouseover', handleMouseOver);
78
109
  target.removeEventListener('mouseleave', handleMouseLeave);
110
+ target.removeEventListener('keydown', handleKeyDown);
111
+ target.removeEventListener('mousemove', handleMouseMove);
79
112
  };
80
113
  }, [
81
114
  editor,
82
115
  handleMouseOver,
83
- handleMouseLeave
116
+ handleMouseLeave,
117
+ activateLeaf
118
+ ]);
119
+ // Recompute button position on every editor update so it tracks the leaf's
120
+ // right edge even when characters are added/removed (changing its width).
121
+ // Deferred to rAF so we read layout after the browser has painted the new DOM.
122
+ useEffect(()=>{
123
+ let rafId = 0;
124
+ const unregister = editor.registerUpdateListener(()=>{
125
+ if (keyboardNavigatingRef.current) return;
126
+ cancelAnimationFrame(rafId);
127
+ rafId = requestAnimationFrame(()=>{
128
+ const el = activeLeafElRef.current;
129
+ if (!el) return;
130
+ const pos = resolvePosition(el.getBoundingClientRect());
131
+ if (!pos) return;
132
+ lastPosRef.current = pos;
133
+ const key = el.getAttribute('data-shipql-key');
134
+ if (key) setHovered({
135
+ key,
136
+ ...pos
137
+ });
138
+ });
139
+ });
140
+ return ()=>{
141
+ cancelAnimationFrame(rafId);
142
+ unregister();
143
+ };
144
+ }, [
145
+ editor,
146
+ resolvePosition
84
147
  ]);
85
148
  const { top, left } = hovered ?? lastPosRef.current;
86
149
  return /*#__PURE__*/ _jsx("div", {
@@ -117,11 +117,20 @@ export function ShipQLPlugin({ onLeafFocus }) {
117
117
  editor.update(()=>{
118
118
  const para = $getRoot().getFirstChild();
119
119
  if (!para) return;
120
- // Find the leaf node's source text from its key.
120
+ // Find the leaf node's source text from its key. When a plain
121
+ // TextNode sits immediately after the leaf (no leading space) the
122
+ // parser treats them as one token — e.g. [Leaf("status:"), Text("s")]
123
+ // parses as "status:s". Include that trailing fragment so
124
+ // removeBySource can match the full AST source.
121
125
  let targetSource = null;
122
126
  for (const child of para.getChildren()){
123
127
  if ($isShipQLLeafNode(child) && child.getKey() === nodeKey) {
124
128
  targetSource = child.getTextContent();
129
+ const next = child.getNextSibling();
130
+ if (next && $isTextNode(next) && !$isShipQLLeafNode(next)) {
131
+ const trailing = next.getTextContent().split(' ')[0];
132
+ if (trailing) targetSource += trailing;
133
+ }
125
134
  break;
126
135
  }
127
136
  }
@@ -1,3 +1,3 @@
1
1
  import type { ShipQLEditorInnerProps } from './shipql-editor';
2
- export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder, className, disabled, mode, text, editorKey, isError, onTextChange, onClear, onToggleMode, }: ShipQLEditorInnerProps): import("react/jsx-runtime").JSX.Element;
2
+ export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder, className, disabled, mode, text, editorKey, isError, onTextChange, onClear, onToggleMode, facets, currentFacet, setCurrentFacet, valueSuggestions, isLoadingValueSuggestions, onLeafChange, }: ShipQLEditorInnerProps): import("react/jsx-runtime").JSX.Element;
3
3
  //# sourceMappingURL=shipql-editor-inner.d.ts.map
@@ -5,7 +5,9 @@ import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
5
5
  import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
6
6
  import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin';
7
7
  import { Input } from '../../components/input/index.js';
8
+ import { Popover, PopoverAnchor } from '../../components/popover/index.js';
8
9
  import { $createParagraphNode, $createTextNode, $getRoot } from 'lexical';
10
+ import { useCallback, useRef, useState } from 'react';
9
11
  import { cn } from '../../utils/cn.js';
10
12
  import { Icon } from '../icon/icon.js';
11
13
  import { LeafCloseOverlay } from './lexical/leaf-close-overlay.js';
@@ -13,61 +15,166 @@ import { OnBlurPlugin } from './lexical/on-blur-plugin.js';
13
15
  import { OnTextChangePlugin } from './lexical/on-text-change-plugin.js';
14
16
  import { ShipQLLeafNode } from './lexical/shipql-leaf-node.js';
15
17
  import { ShipQLPlugin } from './lexical/shipql-plugin.js';
16
- const INPUT_CLASSES = 'block w-full rounded-6 bg-background-field-base py-2 pl-7 pr-58 sm:pr-64 text-md text-foreground-neutral-base caret-foreground-neutral-base outline-none focus:border-border-highlights-interactive shadow-button-neutral';
18
+ import { detectFacetContext, normalizeFacets, tryParse } from './suggestions/generate-suggestions.js';
19
+ import { ShipQLSuggestionsDropdown } from './suggestions/shipql-suggestions-dropdown.js';
20
+ import { ShipQLSuggestionsPlugin } from './suggestions/shipql-suggestions-plugin.js';
21
+ const INPUT_CLASSES = 'block w-full rounded-6 bg-background-field-base py-2 pl-32 pr-58 sm:pr-64 text-md text-foreground-neutral-base caret-foreground-neutral-base outline-none focus:border-border-highlights-interactive shadow-button-neutral';
17
22
  const INPUT_ERROR_CLASSES = 'shadow-border-error';
18
23
  const BUTTON_CLASSES = 'shrink-0 text-foreground-neutral-subtle hover:text-foreground-neutral-base transition-all duration-150 flex justify-center items-center cursor-pointer w-28 sm:w-32 h-full';
19
- export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder, className, disabled, mode, text, editorKey, isError, onTextChange, onClear, onToggleMode }) {
24
+ export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder, className, disabled, mode, text, editorKey, isError, onTextChange, onClear, onToggleMode, facets, currentFacet, setCurrentFacet, valueSuggestions, isLoadingValueSuggestions, onLeafChange }) {
25
+ const [suggestionsOpen, setSuggestionsOpen] = useState(false);
26
+ const [selectedIndex, setSelectedIndex] = useState(-1);
27
+ const [items, setItems] = useState([]);
28
+ const [focusedLeafNode, setFocusedLeafNode] = useState(null);
29
+ const [isNegated, setIsNegated] = useState(false);
30
+ const [showSyntaxHelp, setShowSyntaxHelp] = useState(false);
31
+ const isSelectingRef = useRef(false);
32
+ const applyRef = useRef(null);
33
+ const negationPrefixRef = useRef('');
34
+ const hasSuggestions = Boolean(facets && facets.length > 0);
35
+ const showValueActions = Boolean(currentFacet);
36
+ const isRangeContext = items.length === 1 && items[0]?.type === 'range-slider';
37
+ const syntaxHintMode = isRangeContext ? 'range' : 'value';
38
+ const handleToggleNegate = useCallback((negated)=>{
39
+ setIsNegated(negated);
40
+ negationPrefixRef.current = negated ? '-' : '';
41
+ }, []);
42
+ const handleSetCurrentFacet = useCallback((facet)=>{
43
+ setCurrentFacet?.(facet);
44
+ }, [
45
+ setCurrentFacet
46
+ ]);
47
+ const handleLeafFocus = useCallback((node)=>{
48
+ setFocusedLeafNode(node);
49
+ onLeafFocus?.(node);
50
+ }, [
51
+ onLeafFocus
52
+ ]);
53
+ const handleSelect = useCallback((value)=>{
54
+ applyRef.current?.(value);
55
+ }, []);
56
+ const handleToggleSyntaxHelp = useCallback(()=>{
57
+ setShowSyntaxHelp((prev)=>!prev);
58
+ }, []);
20
59
  return /*#__PURE__*/ _jsxs("div", {
21
60
  "data-shipql-editor": true,
22
61
  className: cn('relative', className),
23
62
  children: [
24
- mode === 'editor' ? /*#__PURE__*/ _jsxs(LexicalComposer, {
25
- initialConfig: {
26
- namespace: 'ShipQLEditor',
27
- nodes: [
28
- ShipQLLeafNode
29
- ],
30
- onError: (error)=>{
31
- throw error;
32
- },
33
- editorState: text ? ()=>{
34
- const para = $createParagraphNode();
35
- para.append($createTextNode(text));
36
- $getRoot().append(para);
37
- } : undefined
38
- },
63
+ mode === 'editor' ? /*#__PURE__*/ _jsxs(Popover, {
64
+ open: hasSuggestions && suggestionsOpen,
39
65
  children: [
40
- /*#__PURE__*/ _jsx(PlainTextPlugin, {
41
- contentEditable: /*#__PURE__*/ _jsx(ContentEditable, {
42
- id: "shipql-editor",
43
- "aria-label": "ShipQL query editor",
44
- className: cn(INPUT_CLASSES, isError && INPUT_ERROR_CLASSES, disabled && 'pointer-events-none opacity-50')
45
- }),
46
- placeholder: placeholder ? /*#__PURE__*/ _jsx("div", {
47
- className: "pointer-events-none absolute left-0 top-0 select-none px-7 py-2 text-md text-foreground-neutral-muted",
48
- children: placeholder
49
- }) : null,
50
- ErrorBoundary: LexicalErrorBoundary
51
- }),
52
- /*#__PURE__*/ _jsx(ShipQLPlugin, {
53
- onLeafFocus: onLeafFocus
54
- }),
55
- /*#__PURE__*/ _jsx(OnBlurPlugin, {
56
- onChange: onChange
66
+ /*#__PURE__*/ _jsx(PopoverAnchor, {
67
+ className: "w-full",
68
+ children: /*#__PURE__*/ _jsxs(LexicalComposer, {
69
+ initialConfig: {
70
+ namespace: 'ShipQLEditor',
71
+ nodes: [
72
+ ShipQLLeafNode
73
+ ],
74
+ onError: (error)=>{
75
+ throw error;
76
+ },
77
+ editorState: text ? ()=>{
78
+ const para = $createParagraphNode();
79
+ para.append($createTextNode(text));
80
+ $getRoot().append(para);
81
+ } : undefined
82
+ },
83
+ children: [
84
+ /*#__PURE__*/ _jsx(PlainTextPlugin, {
85
+ contentEditable: /*#__PURE__*/ _jsxs("div", {
86
+ className: "relative",
87
+ children: [
88
+ /*#__PURE__*/ _jsx("div", {
89
+ className: "absolute left-0 top-4 select-none px-8 py-2 text-md text-foreground-neutral-muted",
90
+ children: /*#__PURE__*/ _jsx(Icon, {
91
+ name: isLoadingValueSuggestions ? 'spinner' : 'searchLine',
92
+ size: 16,
93
+ className: "shrink-0 text-foreground-neutral-muted"
94
+ })
95
+ }),
96
+ /*#__PURE__*/ _jsx(ContentEditable, {
97
+ id: "shipql-editor",
98
+ "aria-label": "ShipQL query editor",
99
+ className: cn(INPUT_CLASSES, isError && INPUT_ERROR_CLASSES, disabled && 'pointer-events-none opacity-50')
100
+ })
101
+ ]
102
+ }),
103
+ placeholder: placeholder ? /*#__PURE__*/ _jsx("div", {
104
+ className: "pointer-events-none absolute left-0 top-0 select-none pl-32 pr-8 py-2 text-md text-foreground-neutral-muted",
105
+ children: placeholder
106
+ }) : null,
107
+ ErrorBoundary: LexicalErrorBoundary
108
+ }),
109
+ /*#__PURE__*/ _jsx(ShipQLPlugin, {
110
+ onLeafFocus: handleLeafFocus
111
+ }),
112
+ /*#__PURE__*/ _jsx(OnBlurPlugin, {
113
+ onChange: onChange
114
+ }),
115
+ /*#__PURE__*/ _jsx(OnTextChangePlugin, {
116
+ onTextChange: onTextChange
117
+ }),
118
+ /*#__PURE__*/ _jsx(HistoryPlugin, {}),
119
+ !disabled && /*#__PURE__*/ _jsx(LeafCloseOverlay, {}),
120
+ hasSuggestions && /*#__PURE__*/ _jsx(ShipQLSuggestionsPlugin, {
121
+ facets: facets ?? [],
122
+ currentFacet: currentFacet ?? null,
123
+ setCurrentFacet: handleSetCurrentFacet,
124
+ valueSuggestions: valueSuggestions ?? [],
125
+ isLoadingValueSuggestions: isLoadingValueSuggestions ?? false,
126
+ open: suggestionsOpen,
127
+ setOpen: setSuggestionsOpen,
128
+ selectedIndex: selectedIndex,
129
+ setSelectedIndex: setSelectedIndex,
130
+ items: items,
131
+ setItems: setItems,
132
+ isSelectingRef: isSelectingRef,
133
+ applyRef: applyRef,
134
+ negationPrefixRef: negationPrefixRef,
135
+ focusedLeafNode: focusedLeafNode,
136
+ onLeafChange: onLeafChange
137
+ })
138
+ ]
139
+ }, editorKey)
57
140
  }),
58
- /*#__PURE__*/ _jsx(OnTextChangePlugin, {
59
- onTextChange: onTextChange
60
- }),
61
- /*#__PURE__*/ _jsx(HistoryPlugin, {}),
62
- !disabled && /*#__PURE__*/ _jsx(LeafCloseOverlay, {})
141
+ hasSuggestions && /*#__PURE__*/ _jsx(ShipQLSuggestionsDropdown, {
142
+ items: items,
143
+ selectedIndex: selectedIndex,
144
+ isSelectingRef: isSelectingRef,
145
+ onSelect: handleSelect,
146
+ isLoading: isLoadingValueSuggestions,
147
+ isNegated: isNegated,
148
+ onToggleNegate: handleToggleNegate,
149
+ showValueActions: showValueActions,
150
+ showSyntaxHelp: showSyntaxHelp,
151
+ onToggleSyntaxHelp: handleToggleSyntaxHelp,
152
+ isError: isError,
153
+ syntaxHintMode: syntaxHintMode
154
+ })
63
155
  ]
64
- }, editorKey) : /*#__PURE__*/ _jsx(Input, {
156
+ }) : /*#__PURE__*/ _jsx(Input, {
65
157
  ref: (el)=>el?.focus(),
66
158
  "aria-label": "ShipQL query editor",
159
+ iconLeft: /*#__PURE__*/ _jsx(Icon, {
160
+ name: "searchLine",
161
+ size: 16,
162
+ className: "shrink-0 text-foreground-neutral-muted"
163
+ }),
67
164
  className: cn(INPUT_CLASSES, disabled && 'pointer-events-none opacity-50'),
68
165
  "aria-invalid": isError,
69
166
  value: text,
70
- onChange: (e)=>onTextChange(e.target.value),
167
+ onChange: (e)=>{
168
+ const newText = e.target.value;
169
+ onTextChange(newText);
170
+ const facetNames = facets ? normalizeFacets(facets) : [];
171
+ const facetCtx = detectFacetContext(newText, facetNames);
172
+ setCurrentFacet?.(facetCtx?.facet ?? null);
173
+ onLeafChange?.({
174
+ partialValue: facetCtx?.partialValue ?? '',
175
+ ast: tryParse(newText)
176
+ });
177
+ },
71
178
  placeholder: placeholder,
72
179
  disabled: disabled
73
180
  }),
@@ -1,5 +1,11 @@
1
1
  import type { AstNode } from '@shipfox/shipql-parser';
2
2
  import type { LeafAstNode } from './lexical/shipql-leaf-node';
3
+ import type { FacetDef } from './suggestions/types';
4
+ export type { FacetDef, RangeFacetConfig } from './suggestions/types';
5
+ export type LeafChangePayload = {
6
+ partialValue: string;
7
+ ast: AstNode | null;
8
+ };
3
9
  export interface ShipQLEditorProps {
4
10
  defaultValue?: string;
5
11
  onChange?: (ast: AstNode) => void;
@@ -7,6 +13,12 @@ export interface ShipQLEditorProps {
7
13
  placeholder?: string;
8
14
  className?: string;
9
15
  disabled?: boolean;
16
+ facets?: FacetDef[];
17
+ currentFacet?: string | null;
18
+ setCurrentFacet?: (facet: string | null) => void;
19
+ valueSuggestions?: string[];
20
+ isLoadingValueSuggestions?: boolean;
21
+ onLeafChange?: (payload: LeafChangePayload) => void;
10
22
  }
11
23
  export interface ShipQLEditorInnerProps extends ShipQLEditorProps {
12
24
  mode: 'editor' | 'text';
@@ -47,7 +47,7 @@ export function ShipQLEditor({ disabled, className, ...props }) {
47
47
  }, []);
48
48
  return /*#__PURE__*/ _jsx(Suspense, {
49
49
  fallback: /*#__PURE__*/ _jsx("div", {
50
- className: cn('h-8 w-full animate-pulse rounded-6 bg-background-components-base', className)
50
+ className: cn('h-28 w-full animate-pulse rounded-6 bg-background-components-base', className)
51
51
  }),
52
52
  children: /*#__PURE__*/ _jsx(ShipQLEditorInner, {
53
53
  ...props,
@@ -0,0 +1,22 @@
1
+ import { type AstNode } from '@shipfox/shipql-parser';
2
+ import type { LeafAstNode } from '../lexical/shipql-leaf-node';
3
+ import type { FacetDef, RangeFacetConfig, SuggestionItem } from './types';
4
+ export declare function tryParse(text: string): AstNode | null;
5
+ export declare function normalizeFacets(facets: FacetDef[]): string[];
6
+ export declare function getFacetConfig(facets: FacetDef[], name: string): RangeFacetConfig | undefined;
7
+ export declare function extractFacetFromLeaf(leaf: LeafAstNode): string | undefined;
8
+ /** Extracts the negation prefix used in a `not` node's source (either 'NOT ' or '-'). */
9
+ export declare function negationPrefixFromSource(source: string): string;
10
+ /** Strips a leading NOT or - negation prefix from activeText and returns it. */
11
+ export declare function stripNegationPrefix(activeText: string): {
12
+ prefix: string;
13
+ stripped: string;
14
+ };
15
+ export interface FacetContext {
16
+ facet: string;
17
+ partialValue: string;
18
+ negationPrefix: string;
19
+ }
20
+ export declare function detectFacetContext(activeText: string, facets: string[]): FacetContext | null;
21
+ export declare function buildSuggestionItems(facets: FacetDef[], valueSuggestions: string[], activeText: string, focusedLeaf: LeafAstNode | null): SuggestionItem[];
22
+ //# sourceMappingURL=generate-suggestions.d.ts.map
@@ -0,0 +1,170 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { parse } from '@shipfox/shipql-parser';
3
+ import { Icon } from '../../../components/icon/index.js';
4
+ // ─── Parse helper ─────────────────────────────────────────────────────────────
5
+ export function tryParse(text) {
6
+ try {
7
+ return parse(text);
8
+ } catch {
9
+ return null;
10
+ }
11
+ }
12
+ // ─── Facet normalization ───────────────────────────────────────────────────────
13
+ export function normalizeFacets(facets) {
14
+ return facets.map((f)=>typeof f === 'string' ? f : f.name);
15
+ }
16
+ export function getFacetConfig(facets, name) {
17
+ for (const f of facets){
18
+ if (typeof f !== 'string' && f.name.toLowerCase() === name.toLowerCase()) return f.config;
19
+ }
20
+ return undefined;
21
+ }
22
+ // ─── Leaf helpers ─────────────────────────────────────────────────────────────
23
+ export function extractFacetFromLeaf(leaf) {
24
+ if (leaf.type === 'match') return leaf.facet;
25
+ if (leaf.type === 'range') return leaf.facet;
26
+ if (leaf.type === 'not') {
27
+ const inner = leaf.expr;
28
+ if (inner.type === 'match' || inner.type === 'range') return inner.facet;
29
+ }
30
+ return undefined;
31
+ }
32
+ function extractValueFromLeaf(leaf) {
33
+ if (leaf.type === 'match') return leaf.value;
34
+ if (leaf.type === 'not' && leaf.expr.type === 'match') return leaf.expr.value;
35
+ return undefined;
36
+ }
37
+ // ─── Negation prefix detection ────────────────────────────────────────────────
38
+ const NOT_PREFIX_RE = /^(NOT\s+)(.*)/i;
39
+ /** Extracts the negation prefix used in a `not` node's source (either 'NOT ' or '-'). */ export function negationPrefixFromSource(source) {
40
+ return source.trimStart().startsWith('-') ? '-' : 'NOT ';
41
+ }
42
+ /** Strips a leading NOT or - negation prefix from activeText and returns it. */ export function stripNegationPrefix(activeText) {
43
+ const notMatch = NOT_PREFIX_RE.exec(activeText);
44
+ if (notMatch) return {
45
+ prefix: 'NOT ',
46
+ stripped: notMatch[2] ?? ''
47
+ };
48
+ if (activeText.startsWith('-')) return {
49
+ prefix: '-',
50
+ stripped: activeText.slice(1)
51
+ };
52
+ return {
53
+ prefix: '',
54
+ stripped: activeText
55
+ };
56
+ }
57
+ export function detectFacetContext(activeText, facets) {
58
+ const { prefix, stripped } = stripNegationPrefix(activeText);
59
+ const colonIdx = stripped.indexOf(':');
60
+ if (colonIdx <= 0) return null;
61
+ const candidate = stripped.slice(0, colonIdx).trim().toLowerCase();
62
+ const facet = facets.find((f)=>f.toLowerCase() === candidate);
63
+ if (!facet) return null;
64
+ return {
65
+ facet,
66
+ partialValue: stripped.slice(colonIdx + 1),
67
+ negationPrefix: prefix
68
+ };
69
+ }
70
+ // ─── Suggestion builder ───────────────────────────────────────────────────────
71
+ export function buildSuggestionItems(facets, valueSuggestions, activeText, focusedLeaf) {
72
+ const facetNames = normalizeFacets(facets);
73
+ const header = (label)=>({
74
+ value: `__header__${label}`,
75
+ label,
76
+ icon: null,
77
+ selected: false,
78
+ type: 'section-header'
79
+ });
80
+ // Focused leaf — show values with current value marked selected.
81
+ // Text-type leaves (bare words) are not facet:value matches, so fall through
82
+ // to facet filtering using the leaf's text as the partial query.
83
+ if (focusedLeaf && focusedLeaf.type !== 'text') {
84
+ const facetName = extractFacetFromLeaf(focusedLeaf) ?? '';
85
+ const rangeCfg = getFacetConfig(facets, facetName);
86
+ if (rangeCfg) {
87
+ return [
88
+ {
89
+ value: `__range__${facetName}`,
90
+ label: facetName,
91
+ icon: null,
92
+ selected: false,
93
+ type: 'range-slider',
94
+ rangeFacetConfig: rangeCfg,
95
+ facetName
96
+ }
97
+ ];
98
+ }
99
+ const currentValue = extractValueFromLeaf(focusedLeaf);
100
+ if (valueSuggestions.length === 0) return [];
101
+ return [
102
+ header(facetName.toUpperCase()),
103
+ ...valueSuggestions.map((v)=>{
104
+ const selected = v === currentValue;
105
+ return {
106
+ value: v,
107
+ label: v,
108
+ icon: /*#__PURE__*/ _jsx(Icon, {
109
+ name: selected ? 'checkLine' : 'arrowRightLongFill',
110
+ className: selected ? 'size-16 text-foreground-neutral-base' : 'size-16 text-foreground-neutral-subtle'
111
+ }),
112
+ selected
113
+ };
114
+ })
115
+ ];
116
+ }
117
+ const facetCtx = detectFacetContext(activeText, facetNames);
118
+ // Typing field:value — check for range-type facet first
119
+ if (facetCtx) {
120
+ const rangeCfg = getFacetConfig(facets, facetCtx.facet);
121
+ if (rangeCfg) {
122
+ return [
123
+ {
124
+ value: `__range__${facetCtx.facet}`,
125
+ label: facetCtx.facet,
126
+ icon: null,
127
+ selected: false,
128
+ type: 'range-slider',
129
+ rangeFacetConfig: rangeCfg,
130
+ facetName: facetCtx.facet
131
+ }
132
+ ];
133
+ }
134
+ // Regular facet — show value suggestions (parent is responsible for filtering)
135
+ if (valueSuggestions.length === 0) return [];
136
+ return [
137
+ header(facetCtx.facet.toUpperCase()),
138
+ ...valueSuggestions.map((v)=>({
139
+ value: v,
140
+ label: v,
141
+ icon: /*#__PURE__*/ _jsx(Icon, {
142
+ name: "arrowRightLongFill",
143
+ className: "size-16 text-foreground-neutral-subtle"
144
+ }),
145
+ selected: false
146
+ }))
147
+ ];
148
+ }
149
+ // Otherwise show filtered facets. When cursor is inside a bare-word text
150
+ // leaf chip, use that leaf's value as the filter term. Strip any NOT/- prefix
151
+ // before matching so "NOT sta" still suggests "status", "-sta" suggests "status", etc.
152
+ const rawPartial = focusedLeaf?.type === 'text' ? focusedLeaf.value : activeText;
153
+ const partial = stripNegationPrefix(rawPartial.trim()).stripped.toLowerCase();
154
+ const filtered = partial ? facetNames.filter((f)=>f.toLowerCase().includes(partial)) : facetNames;
155
+ if (filtered.length === 0) return [];
156
+ return [
157
+ header('TYPE'),
158
+ ...filtered.slice(0, 8).map((f)=>({
159
+ value: f,
160
+ label: f,
161
+ icon: /*#__PURE__*/ _jsx(Icon, {
162
+ name: "searchLine",
163
+ className: "size-16 text-foreground-neutral-subtle"
164
+ }),
165
+ selected: false
166
+ }))
167
+ ];
168
+ }
169
+
170
+ //# sourceMappingURL=generate-suggestions.js.map
@@ -0,0 +1,9 @@
1
+ import type { RangeFacetConfig } from './types';
2
+ export interface ShipQLRangeFacetPanelProps {
3
+ facetName: string;
4
+ config: RangeFacetConfig;
5
+ onApply: (value: string) => void;
6
+ isSelectingRef: React.RefObject<boolean>;
7
+ }
8
+ export declare function ShipQLRangeFacetPanel({ facetName, config, onApply, isSelectingRef, }: ShipQLRangeFacetPanelProps): import("react/jsx-runtime").JSX.Element;
9
+ //# sourceMappingURL=shipql-range-facet-panel.d.ts.map