@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.
- package/dist/components/code-block/code-content.js +5 -2
- package/dist/components/dashboard/components/kpi-card.js +2 -2
- package/dist/components/shipql-editor/index.d.ts +1 -1
- package/dist/components/shipql-editor/index.js +1 -1
- package/dist/components/shipql-editor/lexical/leaf-close-overlay.js +65 -2
- package/dist/components/shipql-editor/lexical/shipql-plugin.js +10 -1
- package/dist/components/shipql-editor/shipql-editor-inner.d.ts +1 -1
- package/dist/components/shipql-editor/shipql-editor-inner.js +148 -41
- package/dist/components/shipql-editor/shipql-editor.d.ts +12 -0
- package/dist/components/shipql-editor/shipql-editor.js +1 -1
- package/dist/components/shipql-editor/suggestions/generate-suggestions.d.ts +22 -0
- package/dist/components/shipql-editor/suggestions/generate-suggestions.js +170 -0
- package/dist/components/shipql-editor/suggestions/shipql-range-facet-panel.d.ts +9 -0
- package/dist/components/shipql-editor/suggestions/shipql-range-facet-panel.js +376 -0
- package/dist/components/shipql-editor/suggestions/shipql-suggestion-item.d.ts +11 -0
- package/dist/components/shipql-editor/suggestions/shipql-suggestion-item.js +40 -0
- package/dist/components/shipql-editor/suggestions/shipql-suggestions-dropdown.d.ts +19 -0
- package/dist/components/shipql-editor/suggestions/shipql-suggestions-dropdown.js +128 -0
- package/dist/components/shipql-editor/suggestions/shipql-suggestions-footer.d.ts +11 -0
- package/dist/components/shipql-editor/suggestions/shipql-suggestions-footer.js +123 -0
- package/dist/components/shipql-editor/suggestions/shipql-suggestions-plugin.d.ts +27 -0
- package/dist/components/shipql-editor/suggestions/shipql-suggestions-plugin.js +407 -0
- package/dist/components/shipql-editor/suggestions/types.d.ts +20 -0
- package/dist/components/shipql-editor/suggestions/types.js +3 -0
- package/dist/styles.css +1 -1
- 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)
|
|
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
|
-
},
|
|
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
|
|
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
|
-
},
|
|
56
|
+
}, card.label);
|
|
57
57
|
})
|
|
58
58
|
})
|
|
59
59
|
});
|
|
@@ -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
|
-
|
|
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(
|
|
25
|
-
|
|
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(
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
}
|
|
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)=>
|
|
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-
|
|
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
|