@shipfox/react-ui 0.31.0 → 0.32.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/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/shipql-editor/index.d.ts +1 -1
- package/dist/components/shipql-editor/lexical/shipql-leaf-node.d.ts +3 -2
- package/dist/components/shipql-editor/lexical/shipql-leaf-node.js +8 -4
- package/dist/components/shipql-editor/lexical/shipql-plugin.d.ts +2 -1
- package/dist/components/shipql-editor/lexical/shipql-plugin.js +7 -3
- package/dist/components/shipql-editor/shipql-editor-inner.d.ts +1 -1
- package/dist/components/shipql-editor/shipql-editor-inner.js +7 -2
- package/dist/components/shipql-editor/shipql-editor.d.ts +3 -2
- package/dist/components/shipql-editor/shipql-editor.js +7 -1
- package/dist/components/shipql-editor/suggestions/generate-suggestions.js +1 -1
- package/dist/components/shipql-editor/suggestions/shipql-range-facet-panel.js +98 -62
- package/dist/components/shipql-editor/suggestions/shipql-suggestions-plugin.js +17 -1
- package/dist/components/shipql-editor/suggestions/types.d.ts +2 -0
- package/dist/components/switch/index.d.ts +2 -0
- package/dist/components/switch/index.js +3 -0
- package/dist/components/switch/switch.d.ts +12 -0
- package/dist/components/switch/switch.js +48 -0
- package/dist/components/table/data-table.js +2 -2
- package/dist/styles.css +1 -1
- package/package.json +7 -6
package/dist/components/index.js
CHANGED
|
@@ -38,6 +38,7 @@ export * from './shiny-text/index.js';
|
|
|
38
38
|
export * from './shipql-editor/index.js';
|
|
39
39
|
export * from './skeleton/index.js';
|
|
40
40
|
export * from './slider/index.js';
|
|
41
|
+
export * from './switch/index.js';
|
|
41
42
|
export * from './table/index.js';
|
|
42
43
|
export * from './tabs/index.js';
|
|
43
44
|
export * from './textarea/index.js';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export type { LeafAstNode } from './lexical/shipql-leaf-node';
|
|
2
|
-
export { type FacetDef, type LeafChangePayload, type RangeFacetConfig, ShipQLEditor, type ShipQLEditorProps, } from './shipql-editor';
|
|
2
|
+
export { type FacetDef, type FormatLeafDisplay, type LeafChangePayload, type RangeFacetConfig, ShipQLEditor, type ShipQLEditorProps, } from './shipql-editor';
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -10,7 +10,8 @@ type SerializedShipQLLeafNode = SerializedTextNode & {
|
|
|
10
10
|
};
|
|
11
11
|
export declare class ShipQLLeafNode extends TextNode {
|
|
12
12
|
__shipqlNode: LeafAstNode;
|
|
13
|
-
|
|
13
|
+
__displayText: string | null;
|
|
14
|
+
constructor(text: string, shipqlNode: LeafAstNode, key?: NodeKey, displayText?: string);
|
|
14
15
|
static getType(): string;
|
|
15
16
|
static clone(node: ShipQLLeafNode): ShipQLLeafNode;
|
|
16
17
|
static importJSON(serialized: SerializedShipQLLeafNode): ShipQLLeafNode;
|
|
@@ -23,7 +24,7 @@ export declare class ShipQLLeafNode extends TextNode {
|
|
|
23
24
|
getShipQLNode(): LeafAstNode;
|
|
24
25
|
}
|
|
25
26
|
export declare function $isShipQLLeafNode(node: unknown): node is ShipQLLeafNode;
|
|
26
|
-
export declare function $createShipQLLeafNode(text: string, shipqlNode: LeafAstNode): ShipQLLeafNode;
|
|
27
|
+
export declare function $createShipQLLeafNode(text: string, shipqlNode: LeafAstNode, displayText?: string): ShipQLLeafNode;
|
|
27
28
|
/** Returns true if the AST node qualifies as a visual leaf chip in the editor. */
|
|
28
29
|
export declare function isAstLeafNode(ast: AstNode): ast is LeafAstNode;
|
|
29
30
|
export declare function leafSource(node: LeafAstNode): string;
|
|
@@ -30,15 +30,16 @@ function isValidLeafText(text) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
export class ShipQLLeafNode extends TextNode {
|
|
33
|
-
constructor(text, shipqlNode, key){
|
|
33
|
+
constructor(text, shipqlNode, key, displayText){
|
|
34
34
|
super(text, key);
|
|
35
35
|
this.__shipqlNode = shipqlNode;
|
|
36
|
+
this.__displayText = displayText ?? null;
|
|
36
37
|
}
|
|
37
38
|
static getType() {
|
|
38
39
|
return 'shipql-leaf';
|
|
39
40
|
}
|
|
40
41
|
static clone(node) {
|
|
41
|
-
return new ShipQLLeafNode(node.__text, node.__shipqlNode, node.__key);
|
|
42
|
+
return new ShipQLLeafNode(node.__text, node.__shipqlNode, node.__key, node.__displayText ?? undefined);
|
|
42
43
|
}
|
|
43
44
|
static importJSON(serialized) {
|
|
44
45
|
const text = serialized.text;
|
|
@@ -76,6 +77,9 @@ export class ShipQLLeafNode extends TextNode {
|
|
|
76
77
|
}
|
|
77
78
|
createDOM(config) {
|
|
78
79
|
const element = super.createDOM(config);
|
|
80
|
+
if (this.__displayText) {
|
|
81
|
+
element.textContent = this.__displayText;
|
|
82
|
+
}
|
|
79
83
|
const valid = isValidLeafText(this.__text);
|
|
80
84
|
for (const cls of LEAF_BASE_CLASSES.split(' '))element.classList.add(cls);
|
|
81
85
|
for (const cls of (valid ? LEAF_NORMAL_CLASSES : LEAF_ERROR_CLASSES).split(' '))element.classList.add(cls);
|
|
@@ -111,8 +115,8 @@ export class ShipQLLeafNode extends TextNode {
|
|
|
111
115
|
export function $isShipQLLeafNode(node) {
|
|
112
116
|
return node instanceof ShipQLLeafNode;
|
|
113
117
|
}
|
|
114
|
-
export function $createShipQLLeafNode(text, shipqlNode) {
|
|
115
|
-
return new ShipQLLeafNode(text, shipqlNode);
|
|
118
|
+
export function $createShipQLLeafNode(text, shipqlNode, displayText) {
|
|
119
|
+
return new ShipQLLeafNode(text, shipqlNode, undefined, displayText);
|
|
116
120
|
}
|
|
117
121
|
/** Returns true if the AST node qualifies as a visual leaf chip in the editor. */ export function isAstLeafNode(ast) {
|
|
118
122
|
return isSimpleLeaf(ast) || isNotLeaf(ast) || isGroupedCompound(ast);
|
|
@@ -3,7 +3,8 @@ import { type LeafAstNode } from './shipql-leaf-node';
|
|
|
3
3
|
export declare const REMOVE_LEAF_COMMAND: import("lexical").LexicalCommand<string>;
|
|
4
4
|
interface ShipQLPluginProps {
|
|
5
5
|
onLeafFocus?: (node: LeafAstNode | null) => void;
|
|
6
|
+
formatLeafDisplay?: (source: string) => string;
|
|
6
7
|
}
|
|
7
|
-
export declare function ShipQLPlugin({ onLeafFocus }: ShipQLPluginProps): null;
|
|
8
|
+
export declare function ShipQLPlugin({ onLeafFocus, formatLeafDisplay }: ShipQLPluginProps): null;
|
|
8
9
|
export {};
|
|
9
10
|
//# sourceMappingURL=shipql-plugin.d.ts.map
|
|
@@ -98,11 +98,13 @@ function getAbsoluteOffset(para, point) {
|
|
|
98
98
|
/** Payload: the Lexical node key of the leaf to remove. */ export const REMOVE_LEAF_COMMAND = createCommand('REMOVE_LEAF_COMMAND');
|
|
99
99
|
// ─── Plugin ───────────────────────────────────────────────────────────────────
|
|
100
100
|
const REBUILD_TAG = 'shipql-rebuild';
|
|
101
|
-
export function ShipQLPlugin({ onLeafFocus }) {
|
|
101
|
+
export function ShipQLPlugin({ onLeafFocus, formatLeafDisplay }) {
|
|
102
102
|
const [editor] = useLexicalComposerContext();
|
|
103
103
|
// Keep latest callback accessible inside Lexical listeners without re-registering.
|
|
104
104
|
const onLeafFocusRef = useRef(onLeafFocus);
|
|
105
105
|
onLeafFocusRef.current = onLeafFocus;
|
|
106
|
+
const formatLeafDisplayRef = useRef(formatLeafDisplay);
|
|
107
|
+
formatLeafDisplayRef.current = formatLeafDisplay;
|
|
106
108
|
// Track the key of the last focused leaf to avoid redundant callbacks.
|
|
107
109
|
const lastFocusedKeyRef = useRef(null);
|
|
108
110
|
useEffect(()=>{
|
|
@@ -233,7 +235,8 @@ export function ShipQLPlugin({ onLeafFocus }) {
|
|
|
233
235
|
const para = $getRoot().getFirstChild();
|
|
234
236
|
if (!para) return;
|
|
235
237
|
para.clear();
|
|
236
|
-
const
|
|
238
|
+
const fmt = formatLeafDisplayRef.current;
|
|
239
|
+
const newNodes = nextSegments.map((seg)=>seg.kind === 'leaf' ? $createShipQLLeafNode(seg.text, seg.node, fmt?.(seg.text)) : $createTextNode(seg.text));
|
|
237
240
|
for (const node of newNodes)para.append(node);
|
|
238
241
|
// Restore cursor to the same absolute character position.
|
|
239
242
|
if (savedOffset >= 0 && newNodes.length > 0) {
|
|
@@ -273,9 +276,10 @@ export function ShipQLPlugin({ onLeafFocus }) {
|
|
|
273
276
|
if (!ast) return;
|
|
274
277
|
const segments = tokenize(text, collectLeaves(ast));
|
|
275
278
|
if (!needsRebuild(children, segments)) return;
|
|
279
|
+
const fmt = formatLeafDisplayRef.current;
|
|
276
280
|
para.clear();
|
|
277
281
|
for (const seg of segments){
|
|
278
|
-
para.append(seg.kind === 'leaf' ? $createShipQLLeafNode(seg.text, seg.node) : $createTextNode(seg.text));
|
|
282
|
+
para.append(seg.kind === 'leaf' ? $createShipQLLeafNode(seg.text, seg.node, fmt?.(seg.text)) : $createTextNode(seg.text));
|
|
279
283
|
}
|
|
280
284
|
});
|
|
281
285
|
return ()=>{
|
|
@@ -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, facets, currentFacet, setCurrentFacet, valueSuggestions, isLoadingValueSuggestions, onLeafChange, }: 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, formatLeafDisplay, }: ShipQLEditorInnerProps): import("react/jsx-runtime").JSX.Element;
|
|
3
3
|
//# sourceMappingURL=shipql-editor-inner.d.ts.map
|
|
@@ -21,7 +21,7 @@ import { ShipQLSuggestionsPlugin } from './suggestions/shipql-suggestions-plugin
|
|
|
21
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';
|
|
22
22
|
const INPUT_ERROR_CLASSES = 'shadow-border-error';
|
|
23
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';
|
|
24
|
-
export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder, className, disabled, mode, text, editorKey, isError, onTextChange, onClear, onToggleMode, facets, currentFacet, setCurrentFacet, valueSuggestions, isLoadingValueSuggestions, onLeafChange }) {
|
|
24
|
+
export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder, className, disabled, mode, text, editorKey, isError, onTextChange, onClear, onToggleMode, facets, currentFacet, setCurrentFacet, valueSuggestions, isLoadingValueSuggestions, onLeafChange, formatLeafDisplay }) {
|
|
25
25
|
const [suggestionsOpen, setSuggestionsOpen] = useState(false);
|
|
26
26
|
const [selectedIndex, setSelectedIndex] = useState(-1);
|
|
27
27
|
const [items, setItems] = useState([]);
|
|
@@ -107,7 +107,8 @@ export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder,
|
|
|
107
107
|
ErrorBoundary: LexicalErrorBoundary
|
|
108
108
|
}),
|
|
109
109
|
/*#__PURE__*/ _jsx(ShipQLPlugin, {
|
|
110
|
-
onLeafFocus: handleLeafFocus
|
|
110
|
+
onLeafFocus: handleLeafFocus,
|
|
111
|
+
formatLeafDisplay: formatLeafDisplay
|
|
111
112
|
}),
|
|
112
113
|
/*#__PURE__*/ _jsx(OnBlurPlugin, {
|
|
113
114
|
onChange: onChange
|
|
@@ -175,6 +176,10 @@ export default function ShipQLEditorInner({ onChange, onLeafFocus, placeholder,
|
|
|
175
176
|
ast: tryParse(newText)
|
|
176
177
|
});
|
|
177
178
|
},
|
|
179
|
+
onBlur: (e)=>{
|
|
180
|
+
const ast = tryParse(e.target.value);
|
|
181
|
+
if (ast) onChange?.(ast);
|
|
182
|
+
},
|
|
178
183
|
placeholder: placeholder,
|
|
179
184
|
disabled: disabled
|
|
180
185
|
}),
|
|
@@ -1,7 +1,7 @@
|
|
|
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';
|
|
3
|
+
import type { FacetDef, FormatLeafDisplay } from './suggestions/types';
|
|
4
|
+
export type { FacetDef, FormatLeafDisplay, RangeFacetConfig } from './suggestions/types';
|
|
5
5
|
export type LeafChangePayload = {
|
|
6
6
|
partialValue: string;
|
|
7
7
|
ast: AstNode | null;
|
|
@@ -19,6 +19,7 @@ export interface ShipQLEditorProps {
|
|
|
19
19
|
valueSuggestions?: string[];
|
|
20
20
|
isLoadingValueSuggestions?: boolean;
|
|
21
21
|
onLeafChange?: (payload: LeafChangePayload) => void;
|
|
22
|
+
formatLeafDisplay?: FormatLeafDisplay;
|
|
22
23
|
}
|
|
23
24
|
export interface ShipQLEditorInnerProps extends ShipQLEditorProps {
|
|
24
25
|
mode: 'editor' | 'text';
|
|
@@ -34,7 +34,13 @@ export function ShipQLEditor({ disabled, className, ...props }) {
|
|
|
34
34
|
textRef.current = '';
|
|
35
35
|
setIsError(false);
|
|
36
36
|
setEditorKey((k)=>k + 1);
|
|
37
|
-
|
|
37
|
+
props.onLeafChange?.({
|
|
38
|
+
partialValue: '',
|
|
39
|
+
ast: null
|
|
40
|
+
});
|
|
41
|
+
}, [
|
|
42
|
+
props.onLeafChange
|
|
43
|
+
]);
|
|
38
44
|
const handleToggleMode = useCallback(()=>{
|
|
39
45
|
setMode((m)=>{
|
|
40
46
|
if (m === 'editor') {
|
|
@@ -155,7 +155,7 @@ export function buildSuggestionItems(facets, valueSuggestions, activeText, focus
|
|
|
155
155
|
if (filtered.length === 0) return [];
|
|
156
156
|
return [
|
|
157
157
|
header('TYPE'),
|
|
158
|
-
...filtered.
|
|
158
|
+
...filtered.map((f)=>({
|
|
159
159
|
value: f,
|
|
160
160
|
label: f,
|
|
161
161
|
icon: /*#__PURE__*/ _jsx(Icon, {
|
|
@@ -5,6 +5,22 @@ import { Slider } from '../../../components/slider/index.js';
|
|
|
5
5
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
6
|
import { cn } from '../../../utils/cn.js';
|
|
7
7
|
const RECENT_MAX = 5;
|
|
8
|
+
const RANGE_BRACKET_RE = /^\[(\S+)\s+TO\s+(\S+)\]$/;
|
|
9
|
+
const RANGE_OP_RE = /^([<>]=?)(.+)$/;
|
|
10
|
+
function formatRangeDisplay(raw, fmt) {
|
|
11
|
+
const bracketMatch = RANGE_BRACKET_RE.exec(raw);
|
|
12
|
+
if (bracketMatch) {
|
|
13
|
+
const min = Number(bracketMatch[1]);
|
|
14
|
+
const max = Number(bracketMatch[2]);
|
|
15
|
+
if (!Number.isNaN(min) && !Number.isNaN(max)) return `[${fmt(min)} TO ${fmt(max)}]`;
|
|
16
|
+
}
|
|
17
|
+
const opMatch = RANGE_OP_RE.exec(raw);
|
|
18
|
+
if (opMatch) {
|
|
19
|
+
const v = Number(opMatch[2]);
|
|
20
|
+
if (!Number.isNaN(v)) return `${opMatch[1]}${fmt(v)}`;
|
|
21
|
+
}
|
|
22
|
+
return raw;
|
|
23
|
+
}
|
|
8
24
|
const INPUT_CLASSES = 'w-40 shrink-0 rounded-4 border border-border-neutral-base-component bg-background-field-base shadow-button-neutral transition-[color,box-shadow] outline-none px-4 py-2 text-center text-xs text-foreground-neutral-base focus-visible:shadow-border-interactive-with-active [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none';
|
|
9
25
|
function getRecentKey(facetName) {
|
|
10
26
|
return `shipql-range-recent-${facetName}`;
|
|
@@ -37,7 +53,7 @@ function clearRecent(facetName) {
|
|
|
37
53
|
// ignore storage errors
|
|
38
54
|
}
|
|
39
55
|
}
|
|
40
|
-
function PresetRow({ value, onClick, isRecent, isHighlighted, rowRef }) {
|
|
56
|
+
function PresetRow({ value, displayValue, onClick, isRecent, isHighlighted, rowRef }) {
|
|
41
57
|
return /*#__PURE__*/ _jsxs("button", {
|
|
42
58
|
ref: rowRef,
|
|
43
59
|
type: "button",
|
|
@@ -53,7 +69,7 @@ function PresetRow({ value, onClick, isRecent, isHighlighted, rowRef }) {
|
|
|
53
69
|
}),
|
|
54
70
|
/*#__PURE__*/ _jsx("span", {
|
|
55
71
|
className: "flex-1 truncate text-sm text-foreground-neutral-subtle",
|
|
56
|
-
children: value
|
|
72
|
+
children: displayValue ?? value
|
|
57
73
|
})
|
|
58
74
|
]
|
|
59
75
|
});
|
|
@@ -61,6 +77,7 @@ function PresetRow({ value, onClick, isRecent, isHighlighted, rowRef }) {
|
|
|
61
77
|
export function ShipQLRangeFacetPanel({ facetName, config, onApply, isSelectingRef }) {
|
|
62
78
|
const absMin = Number(config.min);
|
|
63
79
|
const absMax = Number(config.max);
|
|
80
|
+
const fmt = config.format ?? String;
|
|
64
81
|
const [sliderValues, setSliderValues] = useState([
|
|
65
82
|
absMin,
|
|
66
83
|
absMax
|
|
@@ -71,19 +88,59 @@ export function ShipQLRangeFacetPanel({ facetName, config, onApply, isSelectingR
|
|
|
71
88
|
selectedPresetIndexRef.current = selectedPresetIndex;
|
|
72
89
|
const presetRowRefs = useRef([]);
|
|
73
90
|
// Local string state for inputs so mid-edit typing isn't clamped
|
|
74
|
-
const [minText, setMinText] = useState(
|
|
75
|
-
const [maxText, setMaxText] = useState(
|
|
91
|
+
const [minText, setMinText] = useState(fmt(absMin));
|
|
92
|
+
const [maxText, setMaxText] = useState(fmt(absMax));
|
|
93
|
+
const [isEditingMin, setIsEditingMin] = useState(false);
|
|
94
|
+
const [isEditingMax, setIsEditingMax] = useState(false);
|
|
76
95
|
const [lo, hi] = sliderValues;
|
|
77
|
-
// Keep input text in sync when slider moves
|
|
96
|
+
// Keep input text in sync when slider moves (only when not actively editing)
|
|
78
97
|
useEffect(()=>{
|
|
79
|
-
setMinText(
|
|
98
|
+
if (!isEditingMin) setMinText(fmt(lo));
|
|
80
99
|
}, [
|
|
81
|
-
lo
|
|
100
|
+
lo,
|
|
101
|
+
fmt,
|
|
102
|
+
isEditingMin
|
|
82
103
|
]);
|
|
83
104
|
useEffect(()=>{
|
|
84
|
-
setMaxText(
|
|
105
|
+
if (!isEditingMax) setMaxText(fmt(hi));
|
|
85
106
|
}, [
|
|
86
|
-
hi
|
|
107
|
+
hi,
|
|
108
|
+
fmt,
|
|
109
|
+
isEditingMax
|
|
110
|
+
]);
|
|
111
|
+
const commitMin = useCallback((text)=>{
|
|
112
|
+
setIsEditingMin(false);
|
|
113
|
+
const n = Number(text);
|
|
114
|
+
if (!Number.isNaN(n)) {
|
|
115
|
+
const clamped = Math.max(absMin, Math.min(n, hi));
|
|
116
|
+
setSliderValues([
|
|
117
|
+
clamped,
|
|
118
|
+
hi
|
|
119
|
+
]);
|
|
120
|
+
}
|
|
121
|
+
setMinText(fmt(lo));
|
|
122
|
+
}, [
|
|
123
|
+
absMin,
|
|
124
|
+
hi,
|
|
125
|
+
lo,
|
|
126
|
+
fmt
|
|
127
|
+
]);
|
|
128
|
+
const commitMax = useCallback((text)=>{
|
|
129
|
+
setIsEditingMax(false);
|
|
130
|
+
const n = Number(text);
|
|
131
|
+
if (!Number.isNaN(n)) {
|
|
132
|
+
const clamped = Math.max(lo, Math.min(n, absMax));
|
|
133
|
+
setSliderValues([
|
|
134
|
+
lo,
|
|
135
|
+
clamped
|
|
136
|
+
]);
|
|
137
|
+
}
|
|
138
|
+
setMaxText(fmt(hi));
|
|
139
|
+
}, [
|
|
140
|
+
absMax,
|
|
141
|
+
lo,
|
|
142
|
+
hi,
|
|
143
|
+
fmt
|
|
87
144
|
]);
|
|
88
145
|
// Hold dropdown open for any pointer interaction inside the panel
|
|
89
146
|
const panelRef = useRef(null);
|
|
@@ -109,15 +166,16 @@ export function ShipQLRangeFacetPanel({ facetName, config, onApply, isSelectingR
|
|
|
109
166
|
isSelectingRef
|
|
110
167
|
]);
|
|
111
168
|
const addLabel = useMemo(()=>{
|
|
112
|
-
if (lo === absMin && hi === absMax) return `Add ">=${lo},<=${hi}"`;
|
|
113
|
-
if (lo === absMin) return `Add "<=${hi}"`;
|
|
114
|
-
if (hi === absMax) return `Add ">=${lo}"`;
|
|
115
|
-
return `Add ">=${lo},<=${hi}"`;
|
|
169
|
+
if (lo === absMin && hi === absMax) return `Add ">=${fmt(lo)},<=${fmt(hi)}"`;
|
|
170
|
+
if (lo === absMin) return `Add "<=${fmt(hi)}"`;
|
|
171
|
+
if (hi === absMax) return `Add ">=${fmt(lo)}"`;
|
|
172
|
+
return `Add ">=${fmt(lo)},<=${fmt(hi)}"`;
|
|
116
173
|
}, [
|
|
117
174
|
lo,
|
|
118
175
|
hi,
|
|
119
176
|
absMin,
|
|
120
|
-
absMax
|
|
177
|
+
absMax,
|
|
178
|
+
fmt
|
|
121
179
|
]);
|
|
122
180
|
const buildValue = useCallback(()=>{
|
|
123
181
|
if (lo === absMin && hi === absMax) return `[${lo} TO ${hi}]`;
|
|
@@ -206,44 +264,6 @@ export function ShipQLRangeFacetPanel({ facetName, config, onApply, isSelectingR
|
|
|
206
264
|
}, [
|
|
207
265
|
selectedPresetIndex
|
|
208
266
|
]);
|
|
209
|
-
// Commit min input on blur or Enter
|
|
210
|
-
const commitMin = useCallback(()=>{
|
|
211
|
-
const v = Number(minText);
|
|
212
|
-
if (!Number.isNaN(v)) {
|
|
213
|
-
const clamped = Math.min(Math.max(v, absMin), hi);
|
|
214
|
-
setSliderValues([
|
|
215
|
-
clamped,
|
|
216
|
-
hi
|
|
217
|
-
]);
|
|
218
|
-
setMinText(String(clamped));
|
|
219
|
-
} else {
|
|
220
|
-
setMinText(String(lo));
|
|
221
|
-
}
|
|
222
|
-
}, [
|
|
223
|
-
minText,
|
|
224
|
-
absMin,
|
|
225
|
-
hi,
|
|
226
|
-
lo
|
|
227
|
-
]);
|
|
228
|
-
// Commit max input on blur or Enter
|
|
229
|
-
const commitMax = useCallback(()=>{
|
|
230
|
-
const v = Number(maxText);
|
|
231
|
-
if (!Number.isNaN(v)) {
|
|
232
|
-
const clamped = Math.max(Math.min(v, absMax), lo);
|
|
233
|
-
setSliderValues([
|
|
234
|
-
lo,
|
|
235
|
-
clamped
|
|
236
|
-
]);
|
|
237
|
-
setMaxText(String(clamped));
|
|
238
|
-
} else {
|
|
239
|
-
setMaxText(String(hi));
|
|
240
|
-
}
|
|
241
|
-
}, [
|
|
242
|
-
maxText,
|
|
243
|
-
absMax,
|
|
244
|
-
lo,
|
|
245
|
-
hi
|
|
246
|
-
]);
|
|
247
267
|
return /*#__PURE__*/ _jsxs("div", {
|
|
248
268
|
ref: panelRef,
|
|
249
269
|
className: "flex flex-col",
|
|
@@ -262,14 +282,21 @@ export function ShipQLRangeFacetPanel({ facetName, config, onApply, isSelectingR
|
|
|
262
282
|
className: "flex items-center gap-12",
|
|
263
283
|
children: [
|
|
264
284
|
/*#__PURE__*/ _jsx("input", {
|
|
265
|
-
type: "
|
|
285
|
+
type: "text",
|
|
286
|
+
className: INPUT_CLASSES,
|
|
266
287
|
value: minText,
|
|
267
|
-
onChange: (e)=>
|
|
268
|
-
|
|
288
|
+
onChange: (e)=>{
|
|
289
|
+
setIsEditingMin(true);
|
|
290
|
+
setMinText(e.target.value);
|
|
291
|
+
},
|
|
292
|
+
onBlur: (e)=>commitMin(e.target.value),
|
|
269
293
|
onKeyDown: (e)=>{
|
|
270
|
-
if (e.key === 'Enter')
|
|
294
|
+
if (e.key === 'Enter') e.target.blur();
|
|
271
295
|
},
|
|
272
|
-
|
|
296
|
+
onFocus: ()=>{
|
|
297
|
+
setIsEditingMin(true);
|
|
298
|
+
setMinText(String(lo));
|
|
299
|
+
}
|
|
273
300
|
}),
|
|
274
301
|
/*#__PURE__*/ _jsx(Slider, {
|
|
275
302
|
className: "flex-1",
|
|
@@ -288,14 +315,21 @@ export function ShipQLRangeFacetPanel({ facetName, config, onApply, isSelectingR
|
|
|
288
315
|
}
|
|
289
316
|
}),
|
|
290
317
|
/*#__PURE__*/ _jsx("input", {
|
|
291
|
-
type: "
|
|
318
|
+
type: "text",
|
|
319
|
+
className: INPUT_CLASSES,
|
|
292
320
|
value: maxText,
|
|
293
|
-
onChange: (e)=>
|
|
294
|
-
|
|
321
|
+
onChange: (e)=>{
|
|
322
|
+
setIsEditingMax(true);
|
|
323
|
+
setMaxText(e.target.value);
|
|
324
|
+
},
|
|
325
|
+
onBlur: (e)=>commitMax(e.target.value),
|
|
295
326
|
onKeyDown: (e)=>{
|
|
296
|
-
if (e.key === 'Enter')
|
|
327
|
+
if (e.key === 'Enter') e.target.blur();
|
|
297
328
|
},
|
|
298
|
-
|
|
329
|
+
onFocus: ()=>{
|
|
330
|
+
setIsEditingMax(true);
|
|
331
|
+
setMaxText(String(hi));
|
|
332
|
+
}
|
|
299
333
|
})
|
|
300
334
|
]
|
|
301
335
|
}),
|
|
@@ -336,6 +370,7 @@ export function ShipQLRangeFacetPanel({ facetName, config, onApply, isSelectingR
|
|
|
336
370
|
}),
|
|
337
371
|
recentValues.slice(0, 3).map((v, i)=>/*#__PURE__*/ _jsx(PresetRow, {
|
|
338
372
|
value: v,
|
|
373
|
+
displayValue: formatRangeDisplay(v, fmt),
|
|
339
374
|
onClick: handlePreset,
|
|
340
375
|
isRecent: true,
|
|
341
376
|
isHighlighted: selectedPresetIndex === i,
|
|
@@ -358,6 +393,7 @@ export function ShipQLRangeFacetPanel({ facetName, config, onApply, isSelectingR
|
|
|
358
393
|
const idx = recentValues.slice(0, 3).length + i;
|
|
359
394
|
return /*#__PURE__*/ _jsx(PresetRow, {
|
|
360
395
|
value: v,
|
|
396
|
+
displayValue: formatRangeDisplay(v, fmt),
|
|
361
397
|
onClick: handlePreset,
|
|
362
398
|
isHighlighted: selectedPresetIndex === idx,
|
|
363
399
|
rowRef: (el)=>{
|
|
@@ -3,6 +3,17 @@ import { $createRangeSelection, $createTextNode, $getRoot, $getSelection, $isRan
|
|
|
3
3
|
import { useEffect, useRef } from 'react';
|
|
4
4
|
import { $isShipQLLeafNode } from '../lexical/shipql-leaf-node.js';
|
|
5
5
|
import { buildSuggestionItems, detectFacetContext, extractFacetFromLeaf, negationPrefixFromSource, normalizeFacets, stripNegationPrefix, tryParse } from './generate-suggestions.js';
|
|
6
|
+
const NEEDS_QUOTING = /[\s"()[\]]/;
|
|
7
|
+
const RANGE_SYNTAX = /^(\[.*\s+TO\s+.*\]|[<>]=?.+)$/;
|
|
8
|
+
const ALREADY_QUOTED = /^"[^"]*"$/;
|
|
9
|
+
function quoteIfNeeded(value) {
|
|
10
|
+
if (RANGE_SYNTAX.test(value)) return value;
|
|
11
|
+
if (ALREADY_QUOTED.test(value)) return value;
|
|
12
|
+
if (value === '' || NEEDS_QUOTING.test(value)) {
|
|
13
|
+
return `"${value}"`;
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
6
17
|
function getActiveSegment(para) {
|
|
7
18
|
const children = para.getChildren();
|
|
8
19
|
let active = '';
|
|
@@ -15,6 +26,11 @@ function getActiveSegment(para) {
|
|
|
15
26
|
const leafText = child.getTextContent();
|
|
16
27
|
if (active.length > 0 && active[0] !== ' ' && leafText.includes(':')) {
|
|
17
28
|
active = leafText + active;
|
|
29
|
+
} else if (active.length === 0 && leafText.endsWith(':')) {
|
|
30
|
+
// Cursor is immediately after a facet leaf like "status:" (no trailing
|
|
31
|
+
// text yet). Include the leaf so detectFacetContext can identify the
|
|
32
|
+
// facet and show value suggestions instead of facet-name suggestions.
|
|
33
|
+
active = leafText;
|
|
18
34
|
}
|
|
19
35
|
break;
|
|
20
36
|
}
|
|
@@ -153,7 +169,7 @@ export function ShipQLSuggestionsPlugin({ facets, currentFacet, setCurrentFacet,
|
|
|
153
169
|
editor.update(()=>{
|
|
154
170
|
const para = $getRoot().getFirstChild();
|
|
155
171
|
if (!para) return;
|
|
156
|
-
const insertText = currentFacetRef.current ? `${negationPrefixRef.current}${currentFacetRef.current}:${selectedValue} ` : `${negationPrefixRef.current}${selectedValue}:`;
|
|
172
|
+
const insertText = currentFacetRef.current ? `${negationPrefixRef.current}${currentFacetRef.current}:${quoteIfNeeded(selectedValue)} ` : `${negationPrefixRef.current}${selectedValue}:`;
|
|
157
173
|
// Case 1: Cursor is inside a focused leaf chip — replace the chip in-place
|
|
158
174
|
if (focusedLeafNodeRef.current) {
|
|
159
175
|
const sel = $getSelection();
|
|
@@ -3,11 +3,13 @@ export interface RangeFacetConfig {
|
|
|
3
3
|
min: string;
|
|
4
4
|
max: string;
|
|
5
5
|
presets?: string[];
|
|
6
|
+
format?: (value: number) => string;
|
|
6
7
|
}
|
|
7
8
|
export type FacetDef = string | {
|
|
8
9
|
name: string;
|
|
9
10
|
config: RangeFacetConfig;
|
|
10
11
|
};
|
|
12
|
+
export type FormatLeafDisplay = (source: string) => string;
|
|
11
13
|
export interface SuggestionItem {
|
|
12
14
|
value: string;
|
|
13
15
|
label: React.ReactNode;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as SwitchPrimitive from '@radix-ui/react-switch';
|
|
2
|
+
import { type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import type { ComponentProps } from 'react';
|
|
4
|
+
export declare const switchVariants: (props?: ({
|
|
5
|
+
size?: "sm" | "md" | "lg" | null | undefined;
|
|
6
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
|
+
export declare const switchThumbVariants: (props?: ({
|
|
8
|
+
size?: "sm" | "md" | "lg" | null | undefined;
|
|
9
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
10
|
+
export type SwitchProps = ComponentProps<typeof SwitchPrimitive.Root> & VariantProps<typeof switchVariants>;
|
|
11
|
+
export declare function Switch({ className, size, ...props }: SwitchProps): import("react/jsx-runtime").JSX.Element;
|
|
12
|
+
//# sourceMappingURL=switch.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as SwitchPrimitive from '@radix-ui/react-switch';
|
|
3
|
+
import { cva } from 'class-variance-authority';
|
|
4
|
+
import { cn } from '../../utils/cn.js';
|
|
5
|
+
export const switchVariants = cva('peer inline-flex shrink-0 cursor-pointer items-center rounded-full border-none outline-none transition-colors duration-200', {
|
|
6
|
+
variants: {
|
|
7
|
+
size: {
|
|
8
|
+
sm: 'h-20 w-36',
|
|
9
|
+
md: 'h-24 w-44',
|
|
10
|
+
lg: 'h-28 w-52'
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
defaultVariants: {
|
|
14
|
+
size: 'md'
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
export const switchThumbVariants = cva('pointer-events-none block rounded-full bg-white shadow-button-neutral transition-transform duration-200', {
|
|
18
|
+
variants: {
|
|
19
|
+
size: {
|
|
20
|
+
sm: 'size-16 data-[state=checked]:translate-x-18 data-[state=unchecked]:translate-x-2',
|
|
21
|
+
md: 'size-20 data-[state=checked]:translate-x-22 data-[state=unchecked]:translate-x-2',
|
|
22
|
+
lg: 'size-24 data-[state=checked]:translate-x-26 data-[state=unchecked]:translate-x-2'
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: {
|
|
26
|
+
size: 'md'
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
export function Switch({ className, size, ...props }) {
|
|
30
|
+
return /*#__PURE__*/ _jsx(SwitchPrimitive.Root, {
|
|
31
|
+
"data-slot": "switch",
|
|
32
|
+
className: cn(switchVariants({
|
|
33
|
+
size
|
|
34
|
+
}), // Unchecked state
|
|
35
|
+
'bg-background-switch-off', 'hover:bg-background-switch-off-hover', // Checked state
|
|
36
|
+
'data-[state=checked]:bg-checkbox-checked-bg', 'data-[state=checked]:hover:bg-checkbox-checked-bg-hover', // Focus
|
|
37
|
+
'focus-visible:shadow-checkbox-unchecked-focus', 'data-[state=checked]:focus-visible:shadow-checkbox-checked-focus', // Disabled
|
|
38
|
+
'disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50', className),
|
|
39
|
+
...props,
|
|
40
|
+
children: /*#__PURE__*/ _jsx(SwitchPrimitive.Thumb, {
|
|
41
|
+
className: cn(switchThumbVariants({
|
|
42
|
+
size
|
|
43
|
+
}))
|
|
44
|
+
})
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=switch.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:
|
|
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:
|
|
86
|
+
pagination: pagination ? paginationState : undefined
|
|
87
87
|
}
|
|
88
88
|
});
|
|
89
89
|
const rowModel = table.getRowModel();
|