@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.
@@ -38,6 +38,7 @@ export * from './shiny-text';
38
38
  export * from './shipql-editor';
39
39
  export * from './skeleton';
40
40
  export * from './slider';
41
+ export * from './switch';
41
42
  export * from './table';
42
43
  export * from './tabs';
43
44
  export * from './textarea';
@@ -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
- constructor(text: string, shipqlNode: LeafAstNode, key?: NodeKey);
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 newNodes = nextSegments.map((seg)=>seg.kind === 'leaf' ? $createShipQLLeafNode(seg.text, seg.node) : $createTextNode(seg.text));
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.slice(0, 8).map((f)=>({
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(String(absMin));
75
- const [maxText, setMaxText] = useState(String(absMax));
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(String(lo));
98
+ if (!isEditingMin) setMinText(fmt(lo));
80
99
  }, [
81
- lo
100
+ lo,
101
+ fmt,
102
+ isEditingMin
82
103
  ]);
83
104
  useEffect(()=>{
84
- setMaxText(String(hi));
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: "number",
285
+ type: "text",
286
+ className: INPUT_CLASSES,
266
287
  value: minText,
267
- onChange: (e)=>setMinText(e.target.value),
268
- onBlur: commitMin,
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') commitMin();
294
+ if (e.key === 'Enter') e.target.blur();
271
295
  },
272
- className: INPUT_CLASSES
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: "number",
318
+ type: "text",
319
+ className: INPUT_CLASSES,
292
320
  value: maxText,
293
- onChange: (e)=>setMaxText(e.target.value),
294
- onBlur: commitMax,
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') commitMax();
327
+ if (e.key === 'Enter') e.target.blur();
297
328
  },
298
- className: INPUT_CLASSES
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,2 @@
1
+ export * from './switch';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export * from './switch.js';
2
+
3
+ //# sourceMappingURL=index.js.map
@@ -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: 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();