@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.
@@ -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__*/ jsx(Segment, {
75
- variant: SEGMENT_VARIANT.attribute,
76
- className: "shrink-0",
77
- error: true === effectiveError || effectiveError === SEGMENT_VARIANT.attribute,
78
- onClick: interactive ? (e)=>handleSegmentClick(SEGMENT_VARIANT.attribute, e) : void 0,
79
- onMouseDown: interactive && building ? handleSegmentMouseDown : void 0,
80
- ...segmentEditProps(SEGMENT_VARIANT.attribute),
81
- children: attribute
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 && {
@@ -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 };
@@ -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
- return /*#__PURE__*/ jsx(DropdownMenu, {
53
- open: open && hasResults,
54
- onOpenChange: onOpenChange,
55
- closeOnSelect: false,
56
- positioning: positioning,
57
- highlightedValue: highlightedValue,
58
- onHighlightChange: onHighlightChange,
59
- children: /*#__PURE__*/ jsxs(DropdownMenuContent, {
60
- ref: menuRef,
61
- className: cn('w-[300px] max-h-[430px]', className),
62
- "data-slot": "filter-input-field-menu",
63
- "data-filter-input-menu": "true",
64
- children: [
65
- !filterText && showRecent && /*#__PURE__*/ jsx(RecentSection, {
66
- conditions: limitedRecentConditions,
67
- fields: fields,
68
- onSelect: onSelect,
69
- registerItem: registerItem
70
- }),
71
- !filterText && showSuggestions && !showRecent && /*#__PURE__*/ jsx(SuggestionsSection, {
72
- fields: suggestedFields,
73
- onSelect: onSelect,
74
- registerItem: registerItem
75
- }),
76
- sections.length > 0 ? /*#__PURE__*/ jsx(FieldSections, {
77
- sections: sections,
78
- onSelect: onSelect,
79
- registerItem: registerItem
80
- }) : /*#__PURE__*/ jsx(MenuEmptyState, {}),
81
- !filterText && (onSelectAnd || onSelectOr) && /*#__PURE__*/ jsx(OperatorsSection, {
82
- onSelectAnd: onSelectAnd,
83
- onSelectOr: onSelectOr,
84
- registerItem: registerItem
85
- }),
86
- /*#__PURE__*/ jsxs(DropdownMenuFooter, {
87
- className: "justify-start",
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__*/ jsxs("span", {
90
- className: "flex items-center gap-4",
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(KbdGroup, {
115
+ /*#__PURE__*/ jsxs("span", {
116
+ className: "flex items-center gap-4",
93
117
  children: [
94
- /*#__PURE__*/ jsx(Kbd, {
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
- /*#__PURE__*/ jsx(Kbd, {
98
- children: "↓"
99
- })
128
+ "to navigate"
100
129
  ]
101
130
  }),
102
- "to navigate"
103
- ]
104
- }),
105
- /*#__PURE__*/ jsxs("span", {
106
- className: "flex items-center gap-4",
107
- children: [
108
- /*#__PURE__*/ jsx(KbdGroup, {
109
- children: /*#__PURE__*/ jsx(Kbd, {
110
- children: "↵"
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 tracking-[-0.02em]', {
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.89.0",
3
- "generatedAt": "2026-08-14T11:47:35.825Z",
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 <Code {...args} size='xs'>\n console.log('Extra small code text');\n </Code>\n <Code {...args} size='s'>\n console.log('Small code text');\n </Code>\n <Code {...args} size='m'>\n console.log('Medium code text');\n </Code>\n <Code {...args} size='l'>\n console.log('Large code text');\n </Code>\n </VStack>\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=&apos;{size}&apos; · {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 <Heading {...args} size='sm'>\n Small Heading\n </Heading>\n <Heading {...args} size='md'>\n Medium Heading\n </Heading>\n <Heading {...args} size='lg'>\n Large Heading\n </Heading>\n <Heading {...args} size='xl'>\n Extra Large Heading\n </Heading>\n <Heading {...args} size='2xl'>\n 2XL Heading\n </Heading>\n <Heading {...args} size='3xl'>\n 3XL Heading\n </Heading>\n <Heading {...args} size='4xl'>\n 4XL Heading\n </Heading>\n <Heading {...args} size='5xl'>\n 5XL Heading\n </Heading>\n <Heading {...args} size='6xl'>\n 6XL Heading\n </Heading>\n <Heading {...args} size='7xl'>\n 7XL Heading\n </Heading>\n </VStack>\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=&apos;{size}&apos; · {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 <Text {...args} size='xs'>\n Extra Small Body Text\n </Text>\n <Text {...args} size='sm'>\n Small Body Text\n </Text>\n <Text {...args} size='md'>\n Medium Body Text\n </Text>\n <Text {...args} size='lg'>\n Large Body Text\n </Text>\n <Text {...args} size='xl'>\n Extra Large Body Text\n </Text>\n </VStack>\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=&apos;{size}&apos; · {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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wallarm-org/design-system",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",