@shipfox/react-ui 0.28.0 → 0.30.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/dashboard/pages/analytics-page.js +9 -15
  2. package/dist/components/date-picker/date-picker.js +1 -0
  3. package/dist/components/index.d.ts +1 -0
  4. package/dist/components/index.js +1 -0
  5. package/dist/components/shipql-editor/index.d.ts +3 -0
  6. package/dist/components/shipql-editor/index.js +3 -0
  7. package/dist/components/shipql-editor/lexical/handle-arrow-right-from-leaf.d.ts +9 -0
  8. package/dist/components/shipql-editor/lexical/handle-arrow-right-from-leaf.js +46 -0
  9. package/dist/components/shipql-editor/lexical/leaf-close-overlay.d.ts +2 -0
  10. package/dist/components/shipql-editor/lexical/leaf-close-overlay.js +110 -0
  11. package/dist/components/shipql-editor/lexical/on-blur-plugin.d.ts +7 -0
  12. package/dist/components/shipql-editor/lexical/on-blur-plugin.js +27 -0
  13. package/dist/components/shipql-editor/lexical/on-text-change-plugin.d.ts +6 -0
  14. package/dist/components/shipql-editor/lexical/on-text-change-plugin.js +21 -0
  15. package/dist/components/shipql-editor/lexical/shipql-leaf-node.d.ts +31 -0
  16. package/dist/components/shipql-editor/lexical/shipql-leaf-node.js +124 -0
  17. package/dist/components/shipql-editor/lexical/shipql-plugin.d.ts +9 -0
  18. package/dist/components/shipql-editor/lexical/shipql-plugin.js +285 -0
  19. package/dist/components/shipql-editor/shipql-editor-inner.d.ts +3 -0
  20. package/dist/components/shipql-editor/shipql-editor-inner.js +103 -0
  21. package/dist/components/shipql-editor/shipql-editor.d.ts +21 -0
  22. package/dist/components/shipql-editor/shipql-editor.js +67 -0
  23. package/dist/components/table/table.stories.data.js +10 -5
  24. package/dist/styles.css +1 -1
  25. package/index.css +1051 -0
  26. package/package.json +10 -7
@@ -61,20 +61,14 @@ const performanceData = [
61
61
  dataD: 160
62
62
  }
63
63
  ];
64
- function generateDurationData() {
65
- const count = 40;
66
- const data = [];
67
- for(let i = 0; i < count; i++){
68
- const baseValue = 80 + Math.random() * 120;
69
- const spike = i % 8 === 0 ? Math.random() * 300 : 0;
70
- data.push({
71
- label: String(i + 1),
72
- value: Math.round(baseValue + spike)
73
- });
74
- }
75
- return data;
76
- }
77
- const durationData = generateDurationData();
64
+ // Deterministic bar chart data — sine-wave base with periodic spikes.
65
+ // Avoids Math.random() so the chart renders identically on every run.
66
+ const durationData = Array.from({
67
+ length: 40
68
+ }, (_, i)=>({
69
+ label: String(i + 1),
70
+ value: Math.round(80 + 60 * (Math.sin(i * 1.3 + 0.5) + 1) + (i % 8 === 0 ? 180 : 0))
71
+ }));
78
72
  const KPI_VALUE_REGEX = /^([^\d]*)([\d.,]+)([^\d]*)$/;
79
73
  function parseKpiValue(value) {
80
74
  if (typeof value === 'number') {
@@ -113,7 +107,7 @@ function renderKpiValue(value) {
113
107
  parsed.prefix,
114
108
  /*#__PURE__*/ _jsx(CountUp, {
115
109
  to: parsed.numericValue,
116
- from: 0,
110
+ from: parsed.numericValue,
117
111
  duration: 0.5,
118
112
  className: "inline"
119
113
  }),
@@ -114,6 +114,7 @@ export const DatePicker = /*#__PURE__*/ forwardRef(({ className, variant, size,
114
114
  align: "start",
115
115
  children: /*#__PURE__*/ _jsx(Calendar, {
116
116
  mode: "single",
117
+ defaultMonth: date,
117
118
  selected: date,
118
119
  onSelect: handleSelect,
119
120
  disabled: disabledDates,
@@ -35,6 +35,7 @@ export * from './search';
35
35
  export * from './select';
36
36
  export * from './sheet';
37
37
  export * from './shiny-text';
38
+ export * from './shipql-editor';
38
39
  export * from './skeleton';
39
40
  export * from './slider';
40
41
  export * from './table';
@@ -35,6 +35,7 @@ export * from './search/index.js';
35
35
  export * from './select/index.js';
36
36
  export * from './sheet/index.js';
37
37
  export * from './shiny-text/index.js';
38
+ export * from './shipql-editor/index.js';
38
39
  export * from './skeleton/index.js';
39
40
  export * from './slider/index.js';
40
41
  export * from './table/index.js';
@@ -0,0 +1,3 @@
1
+ export type { LeafAstNode } from './lexical/shipql-leaf-node';
2
+ export * from './shipql-editor';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export * from './shipql-editor.js';
2
+
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,9 @@
1
+ import { type LexicalEditor } from 'lexical';
2
+ /**
3
+ * Handles the right-arrow key when the cursor is on a leaf at the end of the
4
+ * input. Moves the cursor out of the leaf (appending a space node if needed),
5
+ * mirroring the behaviour of pressing space. Returns false when the conditions
6
+ * are not met so the event falls through to Lexical's default handling.
7
+ */
8
+ export declare function handleArrowRightFromLeaf(event: KeyboardEvent | null, editor: LexicalEditor): boolean;
9
+ //# sourceMappingURL=handle-arrow-right-from-leaf.d.ts.map
@@ -0,0 +1,46 @@
1
+ import { $createRangeSelection, $createTextNode, $getRoot, $getSelection, $isRangeSelection, $isTextNode, $setSelection } from 'lexical';
2
+ import { $isShipQLLeafNode } from './shipql-leaf-node.js';
3
+ /**
4
+ * Handles the right-arrow key when the cursor is on a leaf at the end of the
5
+ * input. Moves the cursor out of the leaf (appending a space node if needed),
6
+ * mirroring the behaviour of pressing space. Returns false when the conditions
7
+ * are not met so the event falls through to Lexical's default handling.
8
+ */ export function handleArrowRightFromLeaf(event, editor) {
9
+ let shouldHandle = false;
10
+ editor.getEditorState().read(()=>{
11
+ const sel = $getSelection();
12
+ if (!$isRangeSelection(sel)) return;
13
+ const anchor = sel.anchor.getNode();
14
+ if (!$isShipQLLeafNode(anchor)) return;
15
+ const next = anchor.getNextSibling();
16
+ // Only take over when the leaf is the last meaningful node in the input.
17
+ if (!next || $isTextNode(next) && next.getTextContentSize() === 0) {
18
+ shouldHandle = true;
19
+ }
20
+ });
21
+ if (!shouldHandle) return false;
22
+ event?.preventDefault();
23
+ editor.update(()=>{
24
+ const sel = $getSelection();
25
+ if (!$isRangeSelection(sel)) return;
26
+ const anchor = sel.anchor.getNode();
27
+ if (!$isShipQLLeafNode(anchor)) return;
28
+ const para = $getRoot().getFirstChild();
29
+ if (!para) return;
30
+ const next = anchor.getNextSibling();
31
+ const newSel = $createRangeSelection();
32
+ if (next && $isTextNode(next)) {
33
+ newSel.anchor.set(next.getKey(), 0, 'text');
34
+ newSel.focus.set(next.getKey(), 0, 'text');
35
+ } else {
36
+ const spaceNode = $createTextNode(' ');
37
+ para.append(spaceNode);
38
+ newSel.anchor.set(spaceNode.getKey(), 1, 'text');
39
+ newSel.focus.set(spaceNode.getKey(), 1, 'text');
40
+ }
41
+ $setSelection(newSel);
42
+ });
43
+ return true;
44
+ }
45
+
46
+ //# sourceMappingURL=handle-arrow-right-from-leaf.js.map
@@ -0,0 +1,2 @@
1
+ export declare function LeafCloseOverlay(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=leaf-close-overlay.d.ts.map
@@ -0,0 +1,110 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
3
+ import { useCallback, useEffect, useRef, useState } from 'react';
4
+ import { cn } from '../../../utils/cn.js';
5
+ import { Button } from '../../button/button.js';
6
+ import { REMOVE_LEAF_COMMAND } from './shipql-plugin.js';
7
+ const LEAF_ACTIVE_CLASSES = 'ring-1 ring-border-highlights-interactive rounded-r-none';
8
+ export function LeafCloseOverlay() {
9
+ const [editor] = useLexicalComposerContext();
10
+ const [hovered, setHovered] = useState(null);
11
+ const containerRef = useRef(null);
12
+ const lastPosRef = useRef({
13
+ top: 0,
14
+ left: 0
15
+ });
16
+ const activeLeafElRef = useRef(null);
17
+ const activateLeaf = useCallback((el)=>{
18
+ if (activeLeafElRef.current && activeLeafElRef.current !== el) {
19
+ for (const cls of LEAF_ACTIVE_CLASSES.split(' ')){
20
+ activeLeafElRef.current.classList.remove(cls);
21
+ }
22
+ }
23
+ if (el) {
24
+ for (const cls of LEAF_ACTIVE_CLASSES.split(' ')){
25
+ el.classList.add(cls);
26
+ }
27
+ }
28
+ activeLeafElRef.current = el;
29
+ }, []);
30
+ const resolvePosition = useCallback((leafRect)=>{
31
+ const container = containerRef.current?.parentElement;
32
+ if (!container) return null;
33
+ const containerRect = container.getBoundingClientRect();
34
+ return {
35
+ top: leafRect.top - containerRect.top - 1,
36
+ left: leafRect.right - containerRect.left - 1
37
+ };
38
+ }, []);
39
+ const handleMouseOver = useCallback((e)=>{
40
+ if (containerRef.current?.contains(e.target)) return;
41
+ const target = e.target.closest('[data-shipql-leaf]');
42
+ if (target) {
43
+ const key = target.getAttribute('data-shipql-key');
44
+ if (key) {
45
+ activateLeaf(target);
46
+ const pos = resolvePosition(target.getBoundingClientRect());
47
+ if (pos) {
48
+ lastPosRef.current = pos;
49
+ setHovered({
50
+ key,
51
+ ...pos
52
+ });
53
+ }
54
+ return;
55
+ }
56
+ }
57
+ activateLeaf(null);
58
+ setHovered(null);
59
+ }, [
60
+ activateLeaf,
61
+ resolvePosition
62
+ ]);
63
+ const handleMouseLeave = useCallback(()=>{
64
+ activateLeaf(null);
65
+ setHovered(null);
66
+ }, [
67
+ activateLeaf
68
+ ]);
69
+ useEffect(()=>{
70
+ const rootElement = editor.getRootElement();
71
+ if (!rootElement) return;
72
+ const container = rootElement.closest('[data-shipql-editor]');
73
+ const target = container ?? rootElement;
74
+ target.addEventListener('mouseover', handleMouseOver);
75
+ target.addEventListener('mouseleave', handleMouseLeave);
76
+ return ()=>{
77
+ target.removeEventListener('mouseover', handleMouseOver);
78
+ target.removeEventListener('mouseleave', handleMouseLeave);
79
+ };
80
+ }, [
81
+ editor,
82
+ handleMouseOver,
83
+ handleMouseLeave
84
+ ]);
85
+ const { top, left } = hovered ?? lastPosRef.current;
86
+ return /*#__PURE__*/ _jsx("div", {
87
+ ref: containerRef,
88
+ children: /*#__PURE__*/ _jsx(Button, {
89
+ variant: "danger",
90
+ size: "xs",
91
+ iconLeft: "closeLine",
92
+ "aria-label": "Remove filter",
93
+ className: cn('absolute z-10 min-w-0! p-0! shadow-none! h-23!', 'bg-background-highlight-interactive!', 'overflow-hidden rounded-l-none! rounded-r-6!', 'transition-[width,opacity] duration-150 ease-out', hovered ? 'w-23! opacity-100!' : 'w-0! opacity-0!'),
94
+ style: {
95
+ top,
96
+ left
97
+ },
98
+ onMouseDown: (e)=>{
99
+ e.preventDefault();
100
+ if (hovered) {
101
+ activateLeaf(null);
102
+ editor.dispatchCommand(REMOVE_LEAF_COMMAND, hovered.key);
103
+ setHovered(null);
104
+ }
105
+ }
106
+ })
107
+ });
108
+ }
109
+
110
+ //# sourceMappingURL=leaf-close-overlay.js.map
@@ -0,0 +1,7 @@
1
+ import type { AstNode } from '@shipfox/shipql-parser';
2
+ interface OnBlurPluginProps {
3
+ onChange?: (ast: AstNode) => void;
4
+ }
5
+ export declare function OnBlurPlugin({ onChange }: OnBlurPluginProps): null;
6
+ export {};
7
+ //# sourceMappingURL=on-blur-plugin.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
2
+ import { parse } from '@shipfox/shipql-parser';
3
+ import { $getRoot, BLUR_COMMAND, COMMAND_PRIORITY_LOW } from 'lexical';
4
+ import { useEffect, useRef } from 'react';
5
+ export function OnBlurPlugin({ onChange }) {
6
+ const [editor] = useLexicalComposerContext();
7
+ // Keep latest callback accessible without re-registering the command.
8
+ const onChangeRef = useRef(onChange);
9
+ onChangeRef.current = onChange;
10
+ useEffect(()=>{
11
+ return editor.registerCommand(BLUR_COMMAND, ()=>{
12
+ const text = editor.getEditorState().read(()=>$getRoot().getTextContent());
13
+ try {
14
+ const ast = parse(text);
15
+ if (ast) onChangeRef.current?.(ast);
16
+ } catch {
17
+ // Invalid query — do not call onChange.
18
+ }
19
+ return false;
20
+ }, COMMAND_PRIORITY_LOW);
21
+ }, [
22
+ editor
23
+ ]);
24
+ return null;
25
+ }
26
+
27
+ //# sourceMappingURL=on-blur-plugin.js.map
@@ -0,0 +1,6 @@
1
+ interface OnTextChangePluginProps {
2
+ onTextChange: (text: string) => void;
3
+ }
4
+ export declare function OnTextChangePlugin({ onTextChange }: OnTextChangePluginProps): null;
5
+ export {};
6
+ //# sourceMappingURL=on-text-change-plugin.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
2
+ import { $getRoot } from 'lexical';
3
+ import { useEffect, useRef } from 'react';
4
+ export function OnTextChangePlugin({ onTextChange }) {
5
+ const [editor] = useLexicalComposerContext();
6
+ const callbackRef = useRef(onTextChange);
7
+ callbackRef.current = onTextChange;
8
+ useEffect(()=>{
9
+ return editor.registerUpdateListener(({ editorState })=>{
10
+ editorState.read(()=>{
11
+ const text = $getRoot().getTextContent();
12
+ callbackRef.current(text);
13
+ });
14
+ });
15
+ }, [
16
+ editor
17
+ ]);
18
+ return null;
19
+ }
20
+
21
+ //# sourceMappingURL=on-text-change-plugin.js.map
@@ -0,0 +1,31 @@
1
+ import { type AstNode } from '@shipfox/shipql-parser';
2
+ import { type EditorConfig, type NodeKey, type SerializedTextNode, TextNode } from 'lexical';
3
+ export type LeafAstNode = AstNode;
4
+ export declare function isSimpleLeaf(ast: AstNode): boolean;
5
+ export declare function isGroupedCompound(ast: AstNode): boolean;
6
+ /** Returns true if a NOT node wraps a simple leaf or grouped compound (single chip). */
7
+ export declare function isNotLeaf(ast: AstNode): boolean;
8
+ type SerializedShipQLLeafNode = SerializedTextNode & {
9
+ type: 'shipql-leaf';
10
+ };
11
+ export declare class ShipQLLeafNode extends TextNode {
12
+ __shipqlNode: LeafAstNode;
13
+ constructor(text: string, shipqlNode: LeafAstNode, key?: NodeKey);
14
+ static getType(): string;
15
+ static clone(node: ShipQLLeafNode): ShipQLLeafNode;
16
+ static importJSON(serialized: SerializedShipQLLeafNode): ShipQLLeafNode;
17
+ exportJSON(): SerializedShipQLLeafNode;
18
+ createDOM(config: EditorConfig): HTMLElement;
19
+ updateDOM(prevNode: this, dom: HTMLElement, config: EditorConfig): boolean;
20
+ isSimpleText(): boolean;
21
+ canInsertTextAfter(): boolean;
22
+ canInsertTextBefore(): boolean;
23
+ getShipQLNode(): LeafAstNode;
24
+ }
25
+ export declare function $isShipQLLeafNode(node: unknown): node is ShipQLLeafNode;
26
+ export declare function $createShipQLLeafNode(text: string, shipqlNode: LeafAstNode): ShipQLLeafNode;
27
+ /** Returns true if the AST node qualifies as a visual leaf chip in the editor. */
28
+ export declare function isAstLeafNode(ast: AstNode): ast is LeafAstNode;
29
+ export declare function leafSource(node: LeafAstNode): string;
30
+ export {};
31
+ //# sourceMappingURL=shipql-leaf-node.d.ts.map
@@ -0,0 +1,124 @@
1
+ import { parse, stringify } from '@shipfox/shipql-parser';
2
+ import { TextNode } from 'lexical';
3
+ const LEAF_BASE_CLASSES = 'whitespace-nowrap rounded-4 mx-1.5 px-4 py-2 text-foreground-neutral-base cursor-text transition-[background-color,box-shadow] duration-150 ease-out';
4
+ const LEAF_NORMAL_CLASSES = 'bg-background-button-neutral-default hover:ring-1 hover:ring-border-highlights-interactive shadow-button-neutral';
5
+ const LEAF_ERROR_CLASSES = 'bg-tag-error-bg hover:bg-tag-error-bg-hover';
6
+ export function isSimpleLeaf(ast) {
7
+ return ast.type === 'match' || ast.type === 'range' || ast.type === 'text';
8
+ }
9
+ /**
10
+ * Returns true if an and/or node represents a grouped compound like
11
+ * `env:(prod OR staging)` or `tag:(web AND api)` — i.e. a facet
12
+ * followed by parenthesised values joined by AND/OR.
13
+ */ const GROUPED_COMPOUND_RE = /^-?[a-zA-Z_][\w.]*:\(.*\)$/;
14
+ export function isGroupedCompound(ast) {
15
+ if (ast.type !== 'and' && ast.type !== 'or') return false;
16
+ return GROUPED_COMPOUND_RE.test(ast.source);
17
+ }
18
+ /** Returns true if a NOT node wraps a simple leaf or grouped compound (single chip). */ export function isNotLeaf(ast) {
19
+ if (ast.type !== 'not') return false;
20
+ return isSimpleLeaf(ast.expr) || isGroupedCompound(ast.expr);
21
+ }
22
+ function isValidLeafText(text) {
23
+ if (!text.trim()) return false;
24
+ try {
25
+ const ast = parse(text);
26
+ if (ast === null) return false;
27
+ return isSimpleLeaf(ast) || isNotLeaf(ast) || isGroupedCompound(ast);
28
+ } catch {
29
+ return false;
30
+ }
31
+ }
32
+ export class ShipQLLeafNode extends TextNode {
33
+ constructor(text, shipqlNode, key){
34
+ super(text, key);
35
+ this.__shipqlNode = shipqlNode;
36
+ }
37
+ static getType() {
38
+ return 'shipql-leaf';
39
+ }
40
+ static clone(node) {
41
+ return new ShipQLLeafNode(node.__text, node.__shipqlNode, node.__key);
42
+ }
43
+ static importJSON(serialized) {
44
+ const text = serialized.text;
45
+ let shipqlNode;
46
+ try {
47
+ const ast = parse(text);
48
+ if (ast !== null && isAstLeafNode(ast)) {
49
+ shipqlNode = ast;
50
+ } else {
51
+ shipqlNode = {
52
+ type: 'text',
53
+ value: text,
54
+ source: text
55
+ };
56
+ }
57
+ } catch {
58
+ shipqlNode = {
59
+ type: 'text',
60
+ value: text,
61
+ source: text
62
+ };
63
+ }
64
+ const node = new ShipQLLeafNode(text, shipqlNode);
65
+ node.setFormat(serialized.format);
66
+ node.setDetail(serialized.detail);
67
+ node.setMode(serialized.mode);
68
+ node.setStyle(serialized.style);
69
+ return node;
70
+ }
71
+ exportJSON() {
72
+ return {
73
+ ...super.exportJSON(),
74
+ type: 'shipql-leaf'
75
+ };
76
+ }
77
+ createDOM(config) {
78
+ const element = super.createDOM(config);
79
+ const valid = isValidLeafText(this.__text);
80
+ for (const cls of LEAF_BASE_CLASSES.split(' '))element.classList.add(cls);
81
+ for (const cls of (valid ? LEAF_NORMAL_CLASSES : LEAF_ERROR_CLASSES).split(' '))element.classList.add(cls);
82
+ element.setAttribute('data-shipql-leaf', 'true');
83
+ element.setAttribute('data-shipql-key', this.__key);
84
+ return element;
85
+ }
86
+ updateDOM(prevNode, dom, config) {
87
+ const result = super.updateDOM(prevNode, dom, config);
88
+ if (prevNode.__text !== this.__text) {
89
+ const prevValid = isValidLeafText(prevNode.__text);
90
+ const nextValid = isValidLeafText(this.__text);
91
+ if (prevValid !== nextValid) {
92
+ for (const cls of (prevValid ? LEAF_NORMAL_CLASSES : LEAF_ERROR_CLASSES).split(' '))dom.classList.remove(cls);
93
+ for (const cls of (nextValid ? LEAF_NORMAL_CLASSES : LEAF_ERROR_CLASSES).split(' '))dom.classList.add(cls);
94
+ }
95
+ }
96
+ return result;
97
+ }
98
+ isSimpleText() {
99
+ return false;
100
+ }
101
+ canInsertTextAfter() {
102
+ return false;
103
+ }
104
+ canInsertTextBefore() {
105
+ return false;
106
+ }
107
+ getShipQLNode() {
108
+ return this.getLatest().__shipqlNode;
109
+ }
110
+ }
111
+ export function $isShipQLLeafNode(node) {
112
+ return node instanceof ShipQLLeafNode;
113
+ }
114
+ export function $createShipQLLeafNode(text, shipqlNode) {
115
+ return new ShipQLLeafNode(text, shipqlNode);
116
+ }
117
+ /** Returns true if the AST node qualifies as a visual leaf chip in the editor. */ export function isAstLeafNode(ast) {
118
+ return isSimpleLeaf(ast) || isNotLeaf(ast) || isGroupedCompound(ast);
119
+ }
120
+ export function leafSource(node) {
121
+ return node.source ?? stringify(node);
122
+ }
123
+
124
+ //# sourceMappingURL=shipql-leaf-node.js.map
@@ -0,0 +1,9 @@
1
+ import { type LeafAstNode } from './shipql-leaf-node';
2
+ /** Payload: the Lexical node key of the leaf to remove. */
3
+ export declare const REMOVE_LEAF_COMMAND: import("lexical").LexicalCommand<string>;
4
+ interface ShipQLPluginProps {
5
+ onLeafFocus?: (node: LeafAstNode | null) => void;
6
+ }
7
+ export declare function ShipQLPlugin({ onLeafFocus }: ShipQLPluginProps): null;
8
+ export {};
9
+ //# sourceMappingURL=shipql-plugin.d.ts.map