@wallarm-org/design-system 1.0.0 → 1.1.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/FilterInput/FilterInputField/ChipsWithGaps.js +1 -0
- package/dist/components/FilterInput/FilterInputField/FilterInputChip/FilterInputChip.d.ts +3 -0
- package/dist/components/FilterInput/FilterInputField/FilterInputChip/FilterInputChip.js +23 -10
- package/dist/components/FilterInput/FilterInputField/FilterInputChip/Segment.d.ts +4 -1
- package/dist/components/FilterInput/FilterInputField/FilterInputChip/Segment.js +2 -1
- package/dist/components/FilterInput/FilterInputMenu/FilterInputFieldMenu/FieldMenuPopover.d.ts +21 -0
- package/dist/components/FilterInput/FilterInputMenu/FilterInputFieldMenu/FieldMenuPopover.js +75 -0
- package/dist/components/FilterInput/FilterInputMenu/FilterInputFieldMenu/FilterInputFieldMenu.js +97 -61
- package/dist/components/FilterInput/FilterInputMenu/hooks/useKeyboardNav.d.ts +1 -0
- package/dist/components/FilterInput/FilterInputMenu/hooks/useKeyboardNav.js +3 -1
- package/dist/components/FilterInput/FilterInputMenu/hooks/useMenuScrollHighlightSync.d.ts +13 -0
- package/dist/components/FilterInput/FilterInputMenu/hooks/useMenuScrollHighlightSync.js +46 -0
- package/dist/components/FilterInput/hooks/useFilterInputExpression/buildChips.js +3 -0
- package/dist/components/FilterInput/types.d.ts +10 -0
- package/dist/components/Heading/Heading.js +11 -11
- package/dist/metadata/components.json +39 -5
- package/dist/theme/Pixel.stories.tsx +79 -0
- package/dist/theme/typography.css +13 -0
- package/package.json +1 -1
|
@@ -22,6 +22,7 @@ const ChipsWithGaps = ({ chips, hideLeadingGap, hideTrailingGap, onChipClick, on
|
|
|
22
22
|
children: /*#__PURE__*/ jsx(FilterInputChip, {
|
|
23
23
|
chipId: chip.id,
|
|
24
24
|
attribute: chip.attribute ?? '',
|
|
25
|
+
attributeDescription: chip.attributeDescription,
|
|
25
26
|
operator: chip.operator,
|
|
26
27
|
value: chip.value,
|
|
27
28
|
error: chip.error,
|
|
@@ -8,6 +8,9 @@ export interface FilterInputChipProps extends Omit<HTMLAttributes<HTMLDivElement
|
|
|
8
8
|
ref?: Ref<HTMLDivElement>;
|
|
9
9
|
chipId?: string;
|
|
10
10
|
attribute: string;
|
|
11
|
+
/** Description shown in a dark tooltip on hover of the attribute segment. When
|
|
12
|
+
* absent, no tooltip renders. The operator/value segments are unaffected (AS-1060). */
|
|
13
|
+
attributeDescription?: string;
|
|
11
14
|
operator?: string;
|
|
12
15
|
value?: string;
|
|
13
16
|
error?: ChipErrorSegment;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useRef } from "react";
|
|
3
3
|
import { cn } from "../../../../utils/cn.js";
|
|
4
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "../../../Tooltip/index.js";
|
|
4
5
|
import { ChipSearchInput } from "./ChipSearchInput.js";
|
|
5
6
|
import { chipVariants, emptyValueHitTarget } from "./classes.js";
|
|
6
7
|
import { useEditingContext } from "./context/EditingContext.js";
|
|
@@ -8,7 +9,7 @@ import { FilterInputRemoveButton } from "./FilterInputRemoveButton.js";
|
|
|
8
9
|
import { PairSeparator } from "./PairSeparator.js";
|
|
9
10
|
import { Segment } from "./Segment.js";
|
|
10
11
|
import { SEGMENT_VARIANT } from "./segmentVariant.js";
|
|
11
|
-
const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = false, valueParts, valueSeparator, errorValueIndices, building = false, disabled = false, pair, onRemove, onSegmentClick, onPairSegmentClick, className, ...props })=>{
|
|
12
|
+
const FilterInputChip = ({ ref, chipId, attribute, attributeDescription, operator, value, error = false, valueParts, valueSeparator, errorValueIndices, building = false, disabled = false, pair, onRemove, onSegmentClick, onPairSegmentClick, className, ...props })=>{
|
|
12
13
|
const interactive = !disabled;
|
|
13
14
|
const internalRef = useRef(null);
|
|
14
15
|
const editing = useEditingContext();
|
|
@@ -50,6 +51,15 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
|
|
|
50
51
|
} : {};
|
|
51
52
|
const baseActiveSegment = 0 === editingSide ? activeSegment : null;
|
|
52
53
|
const pairActiveSegment = 1 === editingSide ? activeSegment : null;
|
|
54
|
+
const attributeSegment = /*#__PURE__*/ jsx(Segment, {
|
|
55
|
+
variant: SEGMENT_VARIANT.attribute,
|
|
56
|
+
className: "shrink-0",
|
|
57
|
+
error: true === effectiveError || effectiveError === SEGMENT_VARIANT.attribute,
|
|
58
|
+
onClick: interactive ? (e)=>handleSegmentClick(SEGMENT_VARIANT.attribute, e) : void 0,
|
|
59
|
+
onMouseDown: interactive && building ? handleSegmentMouseDown : void 0,
|
|
60
|
+
...segmentEditProps(SEGMENT_VARIANT.attribute),
|
|
61
|
+
children: attribute
|
|
62
|
+
});
|
|
53
63
|
const setRefs = useCallback((node)=>{
|
|
54
64
|
internalRef.current = node;
|
|
55
65
|
if ('function' == typeof ref) ref(node);
|
|
@@ -71,15 +81,18 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
|
|
|
71
81
|
},
|
|
72
82
|
...props,
|
|
73
83
|
children: [
|
|
74
|
-
/*#__PURE__*/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
attributeDescription ? /*#__PURE__*/ jsxs(Tooltip, {
|
|
85
|
+
disabled: baseActiveSegment === SEGMENT_VARIANT.attribute,
|
|
86
|
+
children: [
|
|
87
|
+
/*#__PURE__*/ jsx(TooltipTrigger, {
|
|
88
|
+
asChild: true,
|
|
89
|
+
children: attributeSegment
|
|
90
|
+
}),
|
|
91
|
+
/*#__PURE__*/ jsx(TooltipContent, {
|
|
92
|
+
children: attributeDescription
|
|
93
|
+
})
|
|
94
|
+
]
|
|
95
|
+
}) : attributeSegment,
|
|
83
96
|
(operator || baseActiveSegment === SEGMENT_VARIANT.operator) && /*#__PURE__*/ jsx(Segment, {
|
|
84
97
|
variant: SEGMENT_VARIANT.operator,
|
|
85
98
|
className: "shrink-0",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import type { FC, FocusEvent, HTMLAttributes, KeyboardEvent } from 'react';
|
|
1
|
+
import type { FC, FocusEvent, HTMLAttributes, KeyboardEvent, Ref } from 'react';
|
|
2
2
|
import { type SegmentVariant } from './segmentVariant';
|
|
3
3
|
export type SegmentProps = HTMLAttributes<HTMLDivElement> & {
|
|
4
|
+
/** Forwarded to the segment container — lets a Tooltip/Popover trigger anchor
|
|
5
|
+
* to it via `asChild` (attribute-segment chip tooltip, AS-1060). */
|
|
6
|
+
ref?: Ref<HTMLDivElement>;
|
|
4
7
|
variant: SegmentVariant;
|
|
5
8
|
children: string;
|
|
6
9
|
error?: boolean;
|
|
@@ -7,7 +7,7 @@ import { CHAR_WIDTH_PX } from "./constants.js";
|
|
|
7
7
|
import { MultiValueSegment } from "./MultiValueSegment.js";
|
|
8
8
|
import { useSizerWidth } from "./model/useSizerWidth.js";
|
|
9
9
|
import { SEGMENT_VARIANT } from "./segmentVariant.js";
|
|
10
|
-
const Segment = ({ variant, children, className, error, editing, editText, onEditChange, onEditKeyDown, onEditBlur, valueParts, valueSeparator = ', ', errorValueIndices, ...props })=>{
|
|
10
|
+
const Segment = ({ ref, variant, children, className, error, editing, editText, onEditChange, onEditKeyDown, onEditBlur, valueParts, valueSeparator = ', ', errorValueIndices, ...props })=>{
|
|
11
11
|
const textRef = useRef(null);
|
|
12
12
|
const inputRef = useRef(null);
|
|
13
13
|
const sizerRef = useRef(null);
|
|
@@ -58,6 +58,7 @@ const Segment = ({ variant, children, className, error, editing, editText, onEdi
|
|
|
58
58
|
...props
|
|
59
59
|
});
|
|
60
60
|
return /*#__PURE__*/ jsx("div", {
|
|
61
|
+
ref: ref,
|
|
61
62
|
className: cn(segmentContainer, className),
|
|
62
63
|
"data-slot": `segment-${variant}`,
|
|
63
64
|
...isInteractive && {
|
package/dist/components/FilterInput/FilterInputMenu/FilterInputFieldMenu/FieldMenuPopover.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type FC } from 'react';
|
|
2
|
+
export interface FieldMenuPopoverProps {
|
|
3
|
+
/** Menu open AND a described field is highlighted. */
|
|
4
|
+
open: boolean;
|
|
5
|
+
/** Filter name, shown monospace, matching the menu label's casing. */
|
|
6
|
+
title: string;
|
|
7
|
+
/** Short "what it filters by" description. */
|
|
8
|
+
description: string;
|
|
9
|
+
/** Optional monospace example block (wildcards, path patterns, ranges, IDs). */
|
|
10
|
+
example?: string;
|
|
11
|
+
/** Live rect of the highlighted row the popover aligns to. */
|
|
12
|
+
getAnchorRect: () => DOMRect | null;
|
|
13
|
+
/** Highlighted id; a change pokes a reposition as the anchor swaps between rows. */
|
|
14
|
+
repositionKey: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Discovery popover for the field-selection menu: on hover or keyboard focus of a
|
|
18
|
+
* filter attribute, surfaces its title, description, and optional example (AS-1060).
|
|
19
|
+
* Fully controlled by `open` + `getAnchorRect`; no trigger, never takes focus.
|
|
20
|
+
*/
|
|
21
|
+
export declare const FieldMenuPopover: FC<FieldMenuPopoverProps>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { Popover } from "@ark-ui/react";
|
|
4
|
+
import { PopoverContent } from "../../../Popover/index.js";
|
|
5
|
+
import { Separator } from "../../../Separator/index.js";
|
|
6
|
+
import { useFloatingRecomputeOn } from "../../hooks/useFloatingRecomputeOn.js";
|
|
7
|
+
const POPOVER_POSITIONING = {
|
|
8
|
+
placement: 'right-start',
|
|
9
|
+
gutter: 4,
|
|
10
|
+
flip: true,
|
|
11
|
+
overflowPadding: 8
|
|
12
|
+
};
|
|
13
|
+
const FieldMenuPopover = ({ open, title, description, example, getAnchorRect, repositionKey })=>{
|
|
14
|
+
useFloatingRecomputeOn(repositionKey, open);
|
|
15
|
+
useEffect(()=>{
|
|
16
|
+
if (!open) return;
|
|
17
|
+
let raf = 0;
|
|
18
|
+
const onScroll = ()=>{
|
|
19
|
+
if (raf) return;
|
|
20
|
+
raf = requestAnimationFrame(()=>{
|
|
21
|
+
raf = 0;
|
|
22
|
+
window.dispatchEvent(new Event('resize'));
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
window.addEventListener('scroll', onScroll, true);
|
|
26
|
+
return ()=>{
|
|
27
|
+
window.removeEventListener('scroll', onScroll, true);
|
|
28
|
+
if (raf) cancelAnimationFrame(raf);
|
|
29
|
+
};
|
|
30
|
+
}, [
|
|
31
|
+
open
|
|
32
|
+
]);
|
|
33
|
+
return /*#__PURE__*/ jsx(Popover.Root, {
|
|
34
|
+
open: open,
|
|
35
|
+
positioning: {
|
|
36
|
+
...POPOVER_POSITIONING,
|
|
37
|
+
getAnchorRect
|
|
38
|
+
},
|
|
39
|
+
autoFocus: false,
|
|
40
|
+
closeOnInteractOutside: false,
|
|
41
|
+
closeOnEscape: false,
|
|
42
|
+
modal: false,
|
|
43
|
+
lazyMount: true,
|
|
44
|
+
unmountOnExit: true,
|
|
45
|
+
children: /*#__PURE__*/ jsxs(PopoverContent, {
|
|
46
|
+
minWidth: "240px",
|
|
47
|
+
maxWidth: "340px",
|
|
48
|
+
className: "gap-6 pointer-events-none",
|
|
49
|
+
"data-testid": "field-menu-popover",
|
|
50
|
+
children: [
|
|
51
|
+
/*#__PURE__*/ jsx("p", {
|
|
52
|
+
className: "font-mono text-sm leading-sm text-text-primary",
|
|
53
|
+
children: title
|
|
54
|
+
}),
|
|
55
|
+
/*#__PURE__*/ jsx("p", {
|
|
56
|
+
className: "text-xs leading-xs text-text-secondary",
|
|
57
|
+
children: description
|
|
58
|
+
}),
|
|
59
|
+
example && /*#__PURE__*/ jsxs(Fragment, {
|
|
60
|
+
children: [
|
|
61
|
+
/*#__PURE__*/ jsx(Separator, {
|
|
62
|
+
spacing: 8
|
|
63
|
+
}),
|
|
64
|
+
/*#__PURE__*/ jsx("pre", {
|
|
65
|
+
className: "m-0 font-mono text-xs leading-xs text-text-secondary whitespace-pre-wrap",
|
|
66
|
+
children: example
|
|
67
|
+
})
|
|
68
|
+
]
|
|
69
|
+
})
|
|
70
|
+
]
|
|
71
|
+
})
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
FieldMenuPopover.displayName = 'FieldMenuPopover';
|
|
75
|
+
export { FieldMenuPopover };
|
package/dist/components/FilterInput/FilterInputMenu/FilterInputFieldMenu/FilterInputFieldMenu.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo } from "react";
|
|
1
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
3
3
|
import { cn } from "../../../../utils/cn.js";
|
|
4
4
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuFooter } from "../../../DropdownMenu/index.js";
|
|
5
5
|
import { Kbd } from "../../../Kbd/Kbd.js";
|
|
@@ -7,7 +7,9 @@ import { KbdGroup } from "../../../Kbd/KbdGroup.js";
|
|
|
7
7
|
import { buildFieldMenuSections } from "../../lib/index.js";
|
|
8
8
|
import { useFieldMenuNavItems } from "../hooks/useFieldMenuNavItems.js";
|
|
9
9
|
import { useKeyboardNav } from "../hooks/useKeyboardNav.js";
|
|
10
|
+
import { useMenuScrollHighlightSync } from "../hooks/useMenuScrollHighlightSync.js";
|
|
10
11
|
import { MenuEmptyState } from "../MenuEmptyState.js";
|
|
12
|
+
import { FieldMenuPopover } from "./FieldMenuPopover.js";
|
|
11
13
|
import { FieldSections, OperatorsSection, RecentSection, SuggestionsSection } from "./FieldMenuSections.js";
|
|
12
14
|
const FilterInputFieldMenu = ({ fields, filterText = '', onSelect, open = false, onOpenChange, recentConditions = [], suggestedFields = [], fieldGroups, onSelectAnd, onSelectOr, onEscape, positioning, inputRef, menuRef, className })=>{
|
|
13
15
|
const limitedRecentConditions = useMemo(()=>recentConditions.slice(0, 3), [
|
|
@@ -38,7 +40,7 @@ const FilterInputFieldMenu = ({ fields, filterText = '', onSelect, open = false,
|
|
|
38
40
|
} else if ('and' === data.type) onSelectAnd?.();
|
|
39
41
|
else if ('or' === data.type) onSelectOr?.();
|
|
40
42
|
};
|
|
41
|
-
const { highlightedValue, onHighlightChange, registerItem } = useKeyboardNav({
|
|
43
|
+
const { highlightedValue, onHighlightChange, registerItem, getItemElement } = useKeyboardNav({
|
|
42
44
|
items: flatItems,
|
|
43
45
|
open,
|
|
44
46
|
onSelect: handleItemSelect,
|
|
@@ -49,74 +51,108 @@ const FilterInputFieldMenu = ({ fields, filterText = '', onSelect, open = false,
|
|
|
49
51
|
menuRef
|
|
50
52
|
});
|
|
51
53
|
const hasResults = sections.length > 0 || !filterText;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
54
|
+
const highlightedField = useMemo(()=>{
|
|
55
|
+
if (!highlightedValue) return;
|
|
56
|
+
const data = flatItems.find((i)=>i.id === highlightedValue)?.value;
|
|
57
|
+
return data?.type === 'field' ? data.field : void 0;
|
|
58
|
+
}, [
|
|
59
|
+
flatItems,
|
|
60
|
+
highlightedValue
|
|
61
|
+
]);
|
|
62
|
+
const highlightedValueRef = useRef(highlightedValue);
|
|
63
|
+
highlightedValueRef.current = highlightedValue;
|
|
64
|
+
const getPopoverAnchorRect = useCallback(()=>{
|
|
65
|
+
const el = getItemElement(highlightedValueRef.current);
|
|
66
|
+
if (!el) return null;
|
|
67
|
+
const item = el.getBoundingClientRect();
|
|
68
|
+
const menu = el.closest('[data-filter-input-menu="true"]')?.getBoundingClientRect();
|
|
69
|
+
return menu ? new DOMRect(menu.x, item.y, menu.width, item.height) : item;
|
|
70
|
+
}, [
|
|
71
|
+
getItemElement
|
|
72
|
+
]);
|
|
73
|
+
const popoverOpen = open && hasResults && !!highlightedField?.description;
|
|
74
|
+
const menuVisible = open && hasResults;
|
|
75
|
+
useMenuScrollHighlightSync(menuVisible, onHighlightChange);
|
|
76
|
+
return /*#__PURE__*/ jsxs(Fragment, {
|
|
77
|
+
children: [
|
|
78
|
+
/*#__PURE__*/ jsx(DropdownMenu, {
|
|
79
|
+
open: open && hasResults,
|
|
80
|
+
onOpenChange: onOpenChange,
|
|
81
|
+
closeOnSelect: false,
|
|
82
|
+
positioning: positioning,
|
|
83
|
+
highlightedValue: highlightedValue,
|
|
84
|
+
onHighlightChange: onHighlightChange,
|
|
85
|
+
children: /*#__PURE__*/ jsxs(DropdownMenuContent, {
|
|
86
|
+
ref: menuRef,
|
|
87
|
+
className: cn('w-[300px] max-h-[430px]', className),
|
|
88
|
+
"data-slot": "filter-input-field-menu",
|
|
89
|
+
"data-filter-input-menu": "true",
|
|
88
90
|
children: [
|
|
89
|
-
/*#__PURE__*/
|
|
90
|
-
|
|
91
|
+
!filterText && showRecent && /*#__PURE__*/ jsx(RecentSection, {
|
|
92
|
+
conditions: limitedRecentConditions,
|
|
93
|
+
fields: fields,
|
|
94
|
+
onSelect: onSelect,
|
|
95
|
+
registerItem: registerItem
|
|
96
|
+
}),
|
|
97
|
+
!filterText && showSuggestions && !showRecent && /*#__PURE__*/ jsx(SuggestionsSection, {
|
|
98
|
+
fields: suggestedFields,
|
|
99
|
+
onSelect: onSelect,
|
|
100
|
+
registerItem: registerItem
|
|
101
|
+
}),
|
|
102
|
+
sections.length > 0 ? /*#__PURE__*/ jsx(FieldSections, {
|
|
103
|
+
sections: sections,
|
|
104
|
+
onSelect: onSelect,
|
|
105
|
+
registerItem: registerItem
|
|
106
|
+
}) : /*#__PURE__*/ jsx(MenuEmptyState, {}),
|
|
107
|
+
!filterText && (onSelectAnd || onSelectOr) && /*#__PURE__*/ jsx(OperatorsSection, {
|
|
108
|
+
onSelectAnd: onSelectAnd,
|
|
109
|
+
onSelectOr: onSelectOr,
|
|
110
|
+
registerItem: registerItem
|
|
111
|
+
}),
|
|
112
|
+
/*#__PURE__*/ jsxs(DropdownMenuFooter, {
|
|
113
|
+
className: "justify-start",
|
|
91
114
|
children: [
|
|
92
|
-
/*#__PURE__*/ jsxs(
|
|
115
|
+
/*#__PURE__*/ jsxs("span", {
|
|
116
|
+
className: "flex items-center gap-4",
|
|
93
117
|
children: [
|
|
94
|
-
/*#__PURE__*/
|
|
95
|
-
children:
|
|
118
|
+
/*#__PURE__*/ jsxs(KbdGroup, {
|
|
119
|
+
children: [
|
|
120
|
+
/*#__PURE__*/ jsx(Kbd, {
|
|
121
|
+
children: "↑"
|
|
122
|
+
}),
|
|
123
|
+
/*#__PURE__*/ jsx(Kbd, {
|
|
124
|
+
children: "↓"
|
|
125
|
+
})
|
|
126
|
+
]
|
|
96
127
|
}),
|
|
97
|
-
|
|
98
|
-
children: "↓"
|
|
99
|
-
})
|
|
128
|
+
"to navigate"
|
|
100
129
|
]
|
|
101
130
|
}),
|
|
102
|
-
"
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
})
|
|
113
|
-
"to select"
|
|
131
|
+
/*#__PURE__*/ jsxs("span", {
|
|
132
|
+
className: "flex items-center gap-4",
|
|
133
|
+
children: [
|
|
134
|
+
/*#__PURE__*/ jsx(KbdGroup, {
|
|
135
|
+
children: /*#__PURE__*/ jsx(Kbd, {
|
|
136
|
+
children: "↵"
|
|
137
|
+
})
|
|
138
|
+
}),
|
|
139
|
+
"to select"
|
|
140
|
+
]
|
|
141
|
+
})
|
|
114
142
|
]
|
|
115
143
|
})
|
|
116
144
|
]
|
|
117
145
|
})
|
|
118
|
-
|
|
119
|
-
|
|
146
|
+
}),
|
|
147
|
+
/*#__PURE__*/ jsx(FieldMenuPopover, {
|
|
148
|
+
open: popoverOpen,
|
|
149
|
+
title: highlightedField?.label ?? '',
|
|
150
|
+
description: highlightedField?.description ?? '',
|
|
151
|
+
example: highlightedField?.example,
|
|
152
|
+
getAnchorRect: getPopoverAnchorRect,
|
|
153
|
+
repositionKey: highlightedValue
|
|
154
|
+
})
|
|
155
|
+
]
|
|
120
156
|
});
|
|
121
157
|
};
|
|
122
158
|
FilterInputFieldMenu.displayName = 'FilterInputFieldMenu';
|
|
@@ -35,5 +35,6 @@ export declare const useKeyboardNav: ({ items, open, onSelect, onClose, onArrowR
|
|
|
35
35
|
}) => void;
|
|
36
36
|
pendingIds: Set<string>;
|
|
37
37
|
registerItem: (id: string) => (el: HTMLElement | null) => void;
|
|
38
|
+
getItemElement: (id: string) => HTMLElement | null;
|
|
38
39
|
};
|
|
39
40
|
export {};
|
|
@@ -7,6 +7,7 @@ const useKeyboardNav = ({ items, open, onSelect, onClose, onArrowRight, onArrowL
|
|
|
7
7
|
if (el) itemRegistryRef.current.set(id, el);
|
|
8
8
|
else itemRegistryRef.current.delete(id);
|
|
9
9
|
}, []);
|
|
10
|
+
const getItemElement = useCallback((id)=>itemRegistryRef.current.get(id) ?? null, []);
|
|
10
11
|
const activeIndexRef = useRef(-1);
|
|
11
12
|
const pendingIdsRef = useRef(pendingIds);
|
|
12
13
|
pendingIdsRef.current = pendingIds;
|
|
@@ -253,7 +254,8 @@ const useKeyboardNav = ({ items, open, onSelect, onClose, onArrowRight, onArrowL
|
|
|
253
254
|
highlightedValue,
|
|
254
255
|
onHighlightChange,
|
|
255
256
|
pendingIds,
|
|
256
|
-
registerItem
|
|
257
|
+
registerItem,
|
|
258
|
+
getItemElement
|
|
257
259
|
};
|
|
258
260
|
};
|
|
259
261
|
export { useKeyboardNav };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps the field-menu highlight — and the description popover that follows it —
|
|
3
|
+
* on the row under the cursor when the list scrolls beneath a stationary pointer
|
|
4
|
+
* (wheel/trackpad). Ark moves the highlight only on `pointermove`, so a scroll
|
|
5
|
+
* would otherwise strand it. On scroll we hit-test the last pointer and drive the
|
|
6
|
+
* highlight via `onHighlightChange` (a synthetic `pointermove` is ignored by zag
|
|
7
|
+
* when the position is unchanged). Guarded off during arrow-key nav so a resting
|
|
8
|
+
* pointer can't hijack the keyboard. Pass `enabled` for the whole time the menu
|
|
9
|
+
* is visible so the pointer is tracked before the first scroll (AS-1060).
|
|
10
|
+
*/
|
|
11
|
+
export declare const useMenuScrollHighlightSync: (enabled: boolean, onHighlightChange: (details: {
|
|
12
|
+
highlightedValue: string | null;
|
|
13
|
+
}) => void) => void;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
const useMenuScrollHighlightSync = (enabled, onHighlightChange)=>{
|
|
3
|
+
const lastPointerRef = useRef(null);
|
|
4
|
+
const keyboardNavRef = useRef(false);
|
|
5
|
+
useEffect(()=>{
|
|
6
|
+
if (!enabled) return;
|
|
7
|
+
const trackPointer = (e)=>{
|
|
8
|
+
keyboardNavRef.current = false;
|
|
9
|
+
lastPointerRef.current = {
|
|
10
|
+
x: e.clientX,
|
|
11
|
+
y: e.clientY
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
const trackKeyboard = (e)=>{
|
|
15
|
+
if ('ArrowDown' === e.key || 'ArrowUp' === e.key) keyboardNavRef.current = true;
|
|
16
|
+
};
|
|
17
|
+
let raf = 0;
|
|
18
|
+
const onScroll = ()=>{
|
|
19
|
+
if (raf || keyboardNavRef.current) return;
|
|
20
|
+
raf = requestAnimationFrame(()=>{
|
|
21
|
+
raf = 0;
|
|
22
|
+
const p = lastPointerRef.current;
|
|
23
|
+
if (!p) return;
|
|
24
|
+
const item = document.elementFromPoint(p.x, p.y)?.closest('[role="menuitem"]');
|
|
25
|
+
if (!item?.closest('[data-filter-input-menu="true"]')) return;
|
|
26
|
+
const value = item.getAttribute('data-value');
|
|
27
|
+
if (value) onHighlightChange({
|
|
28
|
+
highlightedValue: value
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
window.addEventListener('pointermove', trackPointer, true);
|
|
33
|
+
window.addEventListener('keydown', trackKeyboard, true);
|
|
34
|
+
window.addEventListener('scroll', onScroll, true);
|
|
35
|
+
return ()=>{
|
|
36
|
+
window.removeEventListener('pointermove', trackPointer, true);
|
|
37
|
+
window.removeEventListener('keydown', trackKeyboard, true);
|
|
38
|
+
window.removeEventListener('scroll', onScroll, true);
|
|
39
|
+
if (raf) cancelAnimationFrame(raf);
|
|
40
|
+
};
|
|
41
|
+
}, [
|
|
42
|
+
enabled,
|
|
43
|
+
onHighlightChange
|
|
44
|
+
]);
|
|
45
|
+
};
|
|
46
|
+
export { useMenuScrollHighlightSync };
|
|
@@ -36,6 +36,9 @@ const buildBaseChip = (i, condition, field)=>({
|
|
|
36
36
|
id: chipId(i),
|
|
37
37
|
variant: 'chip',
|
|
38
38
|
attribute: field?.label || condition.field,
|
|
39
|
+
...field?.description && {
|
|
40
|
+
attributeDescription: field.description
|
|
41
|
+
},
|
|
39
42
|
operator: condition.operator ? getOperatorLabel(condition.operator, field?.type || DEFAULT_FIELD_TYPE) : void 0,
|
|
40
43
|
...condition.disabled && {
|
|
41
44
|
disabled: true
|
|
@@ -22,6 +22,11 @@ export interface FilterInputChipData {
|
|
|
22
22
|
id: string;
|
|
23
23
|
variant: FilterInputChipVariant;
|
|
24
24
|
attribute?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Description shown as a dark tooltip on the chip's attribute segment; absent →
|
|
27
|
+
* no tooltip. Recall counterpart of the field-menu popover (AS-1060).
|
|
28
|
+
*/
|
|
29
|
+
attributeDescription?: string;
|
|
25
30
|
operator?: string;
|
|
26
31
|
value?: string;
|
|
27
32
|
error?: ChipErrorSegment;
|
|
@@ -106,6 +111,11 @@ export interface FieldMetadata {
|
|
|
106
111
|
label: string;
|
|
107
112
|
type: FieldType;
|
|
108
113
|
description?: string;
|
|
114
|
+
/**
|
|
115
|
+
* Monospace example block shown under the description in the field-menu popover,
|
|
116
|
+
* for non-obvious value formats (wildcards, ranges, IDs). Multi-line via `\n` (AS-1060).
|
|
117
|
+
*/
|
|
118
|
+
example?: string;
|
|
109
119
|
operators?: FilterOperator[];
|
|
110
120
|
default?: string | number | boolean;
|
|
111
121
|
values?: FieldValueOption[];
|
|
@@ -2,19 +2,19 @@ import { jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Slot } from "@radix-ui/react-slot";
|
|
3
3
|
import { cva } from "class-variance-authority";
|
|
4
4
|
import { cn } from "../../utils/cn.js";
|
|
5
|
-
const headingVariants = cva('font-sans-display text-text-primary
|
|
5
|
+
const headingVariants = cva('font-sans-display text-text-primary', {
|
|
6
6
|
variants: {
|
|
7
7
|
size: {
|
|
8
|
-
sm: 'text-sm',
|
|
9
|
-
md: 'text-base',
|
|
10
|
-
lg: 'text-lg',
|
|
11
|
-
xl: 'text-xl',
|
|
12
|
-
'2xl': 'text-2xl',
|
|
13
|
-
'3xl': 'text-3xl',
|
|
14
|
-
'4xl': 'text-4xl',
|
|
15
|
-
'5xl': 'text-5xl',
|
|
16
|
-
'6xl': 'text-6xl',
|
|
17
|
-
'7xl': 'text-7xl'
|
|
8
|
+
sm: 'text-sm tracking-normal',
|
|
9
|
+
md: 'text-base tracking-normal',
|
|
10
|
+
lg: 'text-lg tracking-normal',
|
|
11
|
+
xl: 'text-xl tracking-xl',
|
|
12
|
+
'2xl': 'text-2xl tracking-2xl',
|
|
13
|
+
'3xl': 'text-3xl tracking-3xl',
|
|
14
|
+
'4xl': 'text-4xl tracking-4xl',
|
|
15
|
+
'5xl': 'text-5xl tracking-5xl',
|
|
16
|
+
'6xl': 'text-6xl tracking-6xl',
|
|
17
|
+
'7xl': 'text-7xl tracking-7xl'
|
|
18
18
|
},
|
|
19
19
|
weight: {
|
|
20
20
|
light: 'font-light',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
3
|
-
"generatedAt": "2026-08-
|
|
2
|
+
"version": "1.0.0",
|
|
3
|
+
"generatedAt": "2026-08-20T14:43:33.279Z",
|
|
4
4
|
"components": [
|
|
5
5
|
{
|
|
6
6
|
"name": "Accordion",
|
|
@@ -17761,7 +17761,7 @@
|
|
|
17761
17761
|
},
|
|
17762
17762
|
{
|
|
17763
17763
|
"name": "Sizes",
|
|
17764
|
-
"code": "({ ...args }) => (\n <VStack align='start'>\n
|
|
17764
|
+
"code": "({ ...args }) => (\n <VStack gap={24} align='start'>\n {SIZES.map(({ size, metrics }) => (\n <VStack key={size} gap={4} align='start'>\n <Text size='xs' color='secondary'>\n size='{size}' · {metrics}\n </Text>\n <Code {...args} size={size}>\n console.log('hello world');\n </Code>\n </VStack>\n ))}\n </VStack>\n)"
|
|
17765
17765
|
},
|
|
17766
17766
|
{
|
|
17767
17767
|
"name": "Weights",
|
|
@@ -30188,6 +30188,12 @@
|
|
|
30188
30188
|
"type": "string",
|
|
30189
30189
|
"required": true
|
|
30190
30190
|
},
|
|
30191
|
+
{
|
|
30192
|
+
"name": "attributeDescription",
|
|
30193
|
+
"type": "string | undefined",
|
|
30194
|
+
"required": false,
|
|
30195
|
+
"description": "Description shown in a dark tooltip on hover of the attribute segment. When\n absent, no tooltip renders. The operator/value segments are unaffected (AS-1060)."
|
|
30196
|
+
},
|
|
30191
30197
|
{
|
|
30192
30198
|
"name": "operator",
|
|
30193
30199
|
"type": "string | undefined",
|
|
@@ -31619,7 +31625,7 @@
|
|
|
31619
31625
|
},
|
|
31620
31626
|
{
|
|
31621
31627
|
"name": "Sizes",
|
|
31622
|
-
"code": "({ ...args }) => (\n <VStack align='start'>\n
|
|
31628
|
+
"code": "({ ...args }) => (\n <VStack gap={24} align='start'>\n {SIZES.map(({ size, metrics }) => (\n <VStack key={size} gap={4} align='start'>\n <Text size='xs' color='secondary'>\n size='{size}' · {metrics}\n </Text>\n <Heading {...args} size={size}>\n The quick brown fox\n </Heading>\n </VStack>\n ))}\n </VStack>\n)"
|
|
31623
31629
|
},
|
|
31624
31630
|
{
|
|
31625
31631
|
"name": "Weights",
|
|
@@ -79101,7 +79107,7 @@
|
|
|
79101
79107
|
},
|
|
79102
79108
|
{
|
|
79103
79109
|
"name": "Sizes",
|
|
79104
|
-
"code": "({ ...args }) => (\n <VStack align='start'>\n
|
|
79110
|
+
"code": "({ ...args }) => (\n <VStack gap={24} align='start'>\n {SIZES.map(({ size, metrics }) => (\n <VStack key={size} gap={4} align='start'>\n <Text size='xs' color='secondary'>\n size='{size}' · {metrics}\n </Text>\n <Text {...args} size={size}>\n The quick brown fox jumps over the lazy dog\n </Text>\n </VStack>\n ))}\n </VStack>\n)"
|
|
79105
79111
|
},
|
|
79106
79112
|
{
|
|
79107
79113
|
"name": "Weights",
|
|
@@ -89983,6 +89989,34 @@
|
|
|
89983
89989
|
{
|
|
89984
89990
|
"name": "--leading-9xl",
|
|
89985
89991
|
"value": "128px"
|
|
89992
|
+
},
|
|
89993
|
+
{
|
|
89994
|
+
"name": "--tracking-xl",
|
|
89995
|
+
"value": "-0.005em"
|
|
89996
|
+
},
|
|
89997
|
+
{
|
|
89998
|
+
"name": "--tracking-2xl",
|
|
89999
|
+
"value": "-0.01em"
|
|
90000
|
+
},
|
|
90001
|
+
{
|
|
90002
|
+
"name": "--tracking-3xl",
|
|
90003
|
+
"value": "-0.01em"
|
|
90004
|
+
},
|
|
90005
|
+
{
|
|
90006
|
+
"name": "--tracking-4xl",
|
|
90007
|
+
"value": "-0.015em"
|
|
90008
|
+
},
|
|
90009
|
+
{
|
|
90010
|
+
"name": "--tracking-5xl",
|
|
90011
|
+
"value": "-0.015em"
|
|
90012
|
+
},
|
|
90013
|
+
{
|
|
90014
|
+
"name": "--tracking-6xl",
|
|
90015
|
+
"value": "-0.02em"
|
|
90016
|
+
},
|
|
90017
|
+
{
|
|
90018
|
+
"name": "--tracking-7xl",
|
|
90019
|
+
"value": "-0.02em"
|
|
89986
90020
|
}
|
|
89987
90021
|
]
|
|
89988
90022
|
},
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from 'storybook-react-rsbuild';
|
|
2
|
+
import { HStack, VStack } from '../components/Stack';
|
|
3
|
+
import { Text } from '../components/Text';
|
|
4
|
+
|
|
5
|
+
const DESCRIPTION = [
|
|
6
|
+
'Geist Pixel is the decorative typeface. Use it for empty states, and for moments that want personality rather than information.',
|
|
7
|
+
'There is no `Pixel` component. Pixel type is composed from the `font-pixel` utility plus the shared `text-*` size utilities, so the steps below are conventions rather than a component API. The only place it ships today is `EmptyStateTitle`, at 16/24.',
|
|
8
|
+
'One cut only: Square at regular weight. There is no light or bold, so `font-bold` on pixel type renders as a browser-synthesized fake — emphasis has to come from size or colour, never from weight.',
|
|
9
|
+
'Not for UI copy, labels, or anything read at length; that is the text ramp. Keep it to short strings, where the blocky letterforms stay legible.',
|
|
10
|
+
].join('\n\n');
|
|
11
|
+
|
|
12
|
+
const meta = {
|
|
13
|
+
title: 'Typography/Pixel',
|
|
14
|
+
parameters: {
|
|
15
|
+
layout: 'padded',
|
|
16
|
+
docs: {
|
|
17
|
+
description: {
|
|
18
|
+
component: DESCRIPTION,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
} satisfies Meta;
|
|
23
|
+
|
|
24
|
+
export default meta;
|
|
25
|
+
|
|
26
|
+
type PixelStep = {
|
|
27
|
+
/** Name of the matching text style in Figma. */
|
|
28
|
+
token: string;
|
|
29
|
+
/** Utilities that reproduce it — note lg is 16px, so it maps to text-base. */
|
|
30
|
+
utilities: string;
|
|
31
|
+
/** Size / leading in px. */
|
|
32
|
+
metrics: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const STEPS: PixelStep[] = [
|
|
36
|
+
{ token: 'pixel-3xl', utilities: 'font-pixel text-3xl', metrics: '30 / 36' },
|
|
37
|
+
{ token: 'pixel-2xl', utilities: 'font-pixel text-2xl', metrics: '24 / 32' },
|
|
38
|
+
{ token: 'pixel-xl', utilities: 'font-pixel text-xl', metrics: '20 / 28' },
|
|
39
|
+
{ token: 'pixel-lg', utilities: 'font-pixel text-base', metrics: '16 / 24' },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
export const Scale: StoryFn<typeof meta> = () => (
|
|
43
|
+
<VStack gap={32} align='start'>
|
|
44
|
+
{STEPS.map(({ token, utilities, metrics }) => (
|
|
45
|
+
<VStack key={token} gap={8} align='start'>
|
|
46
|
+
<HStack gap={12} align='baseline'>
|
|
47
|
+
<Text size='sm' weight='medium'>
|
|
48
|
+
{token}
|
|
49
|
+
</Text>
|
|
50
|
+
<Text size='xs' color='secondary'>
|
|
51
|
+
{metrics} · {utilities}
|
|
52
|
+
</Text>
|
|
53
|
+
</HStack>
|
|
54
|
+
<span className={`${utilities} text-text-primary`}>The quick brown fox</span>
|
|
55
|
+
</VStack>
|
|
56
|
+
))}
|
|
57
|
+
</VStack>
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
export const Decorative: StoryFn<typeof meta> = () => (
|
|
61
|
+
<VStack gap={40} align='start'>
|
|
62
|
+
<VStack gap={12} align='start'>
|
|
63
|
+
<Text size='xs' color='secondary'>
|
|
64
|
+
What it is for — a short, decorative line carrying the personality
|
|
65
|
+
</Text>
|
|
66
|
+
<span className='font-pixel text-base text-text-primary'>No attacks detected</span>
|
|
67
|
+
</VStack>
|
|
68
|
+
|
|
69
|
+
<VStack gap={12} align='start'>
|
|
70
|
+
<Text size='xs' color='secondary'>
|
|
71
|
+
What it is not for — running copy belongs to the text ramp
|
|
72
|
+
</Text>
|
|
73
|
+
<Text size='sm' color='secondary' className='max-w-100'>
|
|
74
|
+
Wallarm did not find any attacks in the selected period. Try widening the time range, or
|
|
75
|
+
check that the filters above are not excluding traffic you expect to see.
|
|
76
|
+
</Text>
|
|
77
|
+
</VStack>
|
|
78
|
+
</VStack>
|
|
79
|
+
);
|
|
@@ -79,4 +79,17 @@
|
|
|
79
79
|
--leading-7xl: 72px;
|
|
80
80
|
--leading-8xl: 96px;
|
|
81
81
|
--leading-9xl: 128px;
|
|
82
|
+
|
|
83
|
+
/* Optical tracking for the heading ramp. Tightens as the size grows, because
|
|
84
|
+
large type sets too loose at the spacing Geist is drawn for.
|
|
85
|
+
Zero below 20px system-wide: at UI sizes the font's own spacing is already
|
|
86
|
+
right, and negative tracking there costs legibility. Steps sm/md/lg get no
|
|
87
|
+
token on purpose — they use tracking-normal. */
|
|
88
|
+
--tracking-xl: -0.005em;
|
|
89
|
+
--tracking-2xl: -0.01em;
|
|
90
|
+
--tracking-3xl: -0.01em;
|
|
91
|
+
--tracking-4xl: -0.015em;
|
|
92
|
+
--tracking-5xl: -0.015em;
|
|
93
|
+
--tracking-6xl: -0.02em;
|
|
94
|
+
--tracking-7xl: -0.02em;
|
|
82
95
|
}
|