@wallarm-org/design-system 0.50.2 → 0.51.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.
@@ -1,17 +1,27 @@
1
1
  import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useRef } from "react";
3
+ import { useControlled } from "../../hooks/index.js";
3
4
  import { cn } from "../../utils/cn.js";
5
+ import { hasTextSelection } from "../../utils/hasTextSelection.js";
4
6
  import { TestIdProvider } from "../../utils/testId.js";
5
7
  import { Tooltip, TooltipContent, TooltipTrigger } from "../Tooltip/index.js";
6
8
  import { buildFullPathLabel } from "./buildFullPathLabel.js";
9
+ import { EXPAND_KEYS } from "./constants.js";
7
10
  import { formatAsFilter } from "./formatAsFilter.js";
8
- import { ParameterPathEllipsis } from "./ParameterPathEllipsis.js";
9
- import { ParameterPathEncoding } from "./ParameterPathEncoding.js";
10
- import { ParameterPathJoint } from "./ParameterPathJoint.js";
11
- import { ParameterPathMethod } from "./ParameterPathMethod.js";
12
- import { ParameterPathSegment } from "./ParameterPathSegment.js";
11
+ import { ParameterPathRow } from "./ParameterPathRow.js";
13
12
  import { useParameterPathTruncation } from "./useParameterPathTruncation.js";
14
- const ParameterPath = ({ ref, method, segments, encoding, attack = false, copyFormat = formatAsFilter, className, 'data-testid': testId, ...rest })=>{
13
+ const ParameterPath = ({ ref, method, segments, encoding, attack = false, expandable = false, expanded, defaultExpanded = false, onExpandedChange, copyFormat = formatAsFilter, className, 'data-testid': testId, ...rest })=>{
14
+ const [expandedState, setExpandedUncontrolled] = useControlled({
15
+ controlled: expanded,
16
+ default: defaultExpanded
17
+ });
18
+ const setExpanded = useCallback((next)=>{
19
+ setExpandedUncontrolled(next);
20
+ onExpandedChange?.(next);
21
+ }, [
22
+ setExpandedUncontrolled,
23
+ onExpandedChange
24
+ ]);
15
25
  const handleCopy = useCallback((event)=>{
16
26
  const text = copyFormat({
17
27
  method,
@@ -36,63 +46,28 @@ const ParameterPath = ({ ref, method, segments, encoding, attack = false, copyFo
36
46
  hasMethod: Boolean(method),
37
47
  hasEncoding: Boolean(encoding)
38
48
  });
39
- const lastIndex = segments.length - 1;
40
- const indices = isTruncated && segments.length > 2 ? visibleSegmentIndices : null;
41
- const renderRow = (forMeasurement)=>{
42
- const visibleIdx = forMeasurement || null === indices ? Array.from({
43
- length: segments.length
44
- }, (_, i)=>i) : indices;
45
- const items = [];
46
- const measure = (slot)=>forMeasurement ? slot : void 0;
47
- if (method) items.push(/*#__PURE__*/ jsx(ParameterPathMethod, {
48
- method: method,
49
- "data-measure": measure('method')
50
- }, "method"));
51
- visibleIdx.forEach((segIndex, position)=>{
52
- const value = segments[segIndex];
53
- if (void 0 === value) return;
54
- const isLast = segIndex === lastIndex;
55
- const isFirstShown = 0 === position;
56
- const showJointBefore = !(isFirstShown && !method);
57
- if (showJointBefore) items.push(/*#__PURE__*/ jsx(ParameterPathJoint, {
58
- "data-measure": measure('joint')
59
- }, `j-${segIndex}-pre`));
60
- const isCollapsedBoundary = !forMeasurement && null !== indices && 0 === position && 2 === visibleIdx.length;
61
- items.push(/*#__PURE__*/ jsx(ParameterPathSegment, {
62
- index: segIndex,
63
- variant: isLast ? 'highlighted' : 'default',
64
- withZap: isLast && attack,
65
- "data-measure": measure('segment'),
66
- children: value
67
- }, `s-${segIndex}`));
68
- if (isCollapsedBoundary) items.push(/*#__PURE__*/ jsx(ParameterPathJoint, {}, "ellipsis-joint-pre"), /*#__PURE__*/ jsx(ParameterPathEllipsis, {}, "ellipsis"));
69
- });
70
- if (encoding) items.push(/*#__PURE__*/ jsx(ParameterPathJoint, {
71
- "data-measure": measure('joint')
72
- }, "enc-joint"), /*#__PURE__*/ jsx(ParameterPathEncoding, {
73
- "data-measure": measure('encoding'),
74
- children: encoding
75
- }, "enc"));
76
- return items;
77
- };
78
- const visibleRow = /*#__PURE__*/ jsx("div", {
79
- ref: containerRef,
80
- "data-row": "visible",
81
- className: "flex items-center gap-0 min-w-0 overflow-hidden",
82
- children: renderRow(false)
83
- });
84
- const measurementRow = /*#__PURE__*/ jsx("div", {
85
- ref: measurementRef,
86
- "data-row": "measure",
87
- "aria-hidden": "true",
88
- className: "flex items-center gap-0 absolute left-[-9999px] top-0 invisible pointer-events-none",
89
- children: /*#__PURE__*/ jsx(TestIdProvider, {
90
- value: void 0,
91
- children: renderRow(true)
92
- })
93
- });
49
+ const collapsible = isTruncated && segments.length > 2;
50
+ const isExpanded = Boolean(expandedState) && collapsible;
51
+ const interactive = expandable && collapsible;
52
+ const indices = collapsible && !isExpanded ? visibleSegmentIndices : null;
53
+ const toggleExpanded = useCallback(()=>{
54
+ if (hasTextSelection()) return;
55
+ setExpanded(!isExpanded);
56
+ }, [
57
+ isExpanded,
58
+ setExpanded
59
+ ]);
60
+ const handleKeyDown = useCallback((event)=>{
61
+ if (EXPAND_KEYS.includes(event.key)) {
62
+ event.preventDefault();
63
+ setExpanded(!isExpanded);
64
+ }
65
+ }, [
66
+ isExpanded,
67
+ setExpanded
68
+ ]);
94
69
  return /*#__PURE__*/ jsxs(Tooltip, {
95
- disabled: !isTruncated,
70
+ disabled: !isTruncated || isExpanded,
96
71
  children: [
97
72
  /*#__PURE__*/ jsx(TooltipTrigger, {
98
73
  asChild: true,
@@ -101,14 +76,37 @@ const ParameterPath = ({ ref, method, segments, encoding, attack = false, copyFo
101
76
  "data-testid": testId,
102
77
  "data-slot": "parameter-path",
103
78
  "data-truncated": isTruncated || void 0,
79
+ "data-expanded": isExpanded || void 0,
104
80
  ref: ref,
105
81
  onCopy: handleCopy,
106
- className: cn('relative flex items-center min-w-0', className),
82
+ role: interactive ? 'button' : void 0,
83
+ tabIndex: interactive ? 0 : void 0,
84
+ "aria-expanded": interactive ? isExpanded : void 0,
85
+ onClick: interactive ? toggleExpanded : void 0,
86
+ onKeyDown: interactive ? handleKeyDown : void 0,
87
+ className: cn('relative flex items-center min-w-0', interactive && 'cursor-pointer', className),
107
88
  children: /*#__PURE__*/ jsxs(TestIdProvider, {
108
89
  value: testId,
109
90
  children: [
110
- visibleRow,
111
- measurementRow
91
+ /*#__PURE__*/ jsx(ParameterPathRow, {
92
+ ref: containerRef,
93
+ forMeasurement: false,
94
+ isExpanded: isExpanded,
95
+ indices: indices,
96
+ method: method,
97
+ segments: segments,
98
+ encoding: encoding,
99
+ attack: attack
100
+ }),
101
+ /*#__PURE__*/ jsx(ParameterPathRow, {
102
+ ref: measurementRef,
103
+ forMeasurement: true,
104
+ indices: null,
105
+ method: method,
106
+ segments: segments,
107
+ encoding: encoding,
108
+ attack: attack
109
+ })
112
110
  ]
113
111
  })
114
112
  })
@@ -0,0 +1,14 @@
1
+ import type { FC, Ref } from 'react';
2
+ import type { HttpMethodName } from '../HttpMethod';
3
+ interface ParameterPathRowProps {
4
+ ref?: Ref<HTMLDivElement>;
5
+ method?: HttpMethodName;
6
+ segments: string[];
7
+ encoding?: string;
8
+ attack: boolean;
9
+ forMeasurement: boolean;
10
+ indices: number[] | null;
11
+ isExpanded?: boolean;
12
+ }
13
+ export declare const ParameterPathRow: FC<ParameterPathRowProps>;
14
+ export {};
@@ -0,0 +1,67 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { range } from "../../utils/range.js";
3
+ import { TestIdProvider } from "../../utils/testId.js";
4
+ import { rowVariants } from "./classes.js";
5
+ import { MEASURE, ROW } from "./constants.js";
6
+ import { ParameterPathEllipsis } from "./ParameterPathEllipsis.js";
7
+ import { ParameterPathEncoding } from "./ParameterPathEncoding.js";
8
+ import { ParameterPathJoint } from "./ParameterPathJoint.js";
9
+ import { ParameterPathMethod } from "./ParameterPathMethod.js";
10
+ import { ParameterPathSegment } from "./ParameterPathSegment.js";
11
+ const ParameterPathRow = ({ ref, method, segments, encoding, attack, forMeasurement, indices, isExpanded = false })=>{
12
+ const lastIndex = segments.length - 1;
13
+ const visibleIdx = forMeasurement || null === indices ? range(segments.length) : indices;
14
+ const measure = (slot)=>forMeasurement ? slot : void 0;
15
+ const items = [];
16
+ if (method) items.push(/*#__PURE__*/ jsx(ParameterPathMethod, {
17
+ method: method,
18
+ "data-measure": measure(MEASURE.method)
19
+ }, "method"));
20
+ visibleIdx.forEach((segIndex, position)=>{
21
+ const value = segments[segIndex];
22
+ if (void 0 === value) return;
23
+ const isLast = segIndex === lastIndex;
24
+ const isFirstShown = 0 === position;
25
+ const showJointBefore = !(isFirstShown && !method);
26
+ if (showJointBefore) items.push(/*#__PURE__*/ jsx(ParameterPathJoint, {
27
+ "data-measure": measure(MEASURE.joint)
28
+ }, `j-${segIndex}-pre`));
29
+ const isCollapsedBoundary = !forMeasurement && null !== indices && 0 === position && 2 === visibleIdx.length;
30
+ items.push(/*#__PURE__*/ jsx(ParameterPathSegment, {
31
+ index: segIndex,
32
+ variant: isLast ? 'highlighted' : 'default',
33
+ withZap: isLast && attack,
34
+ "data-measure": measure(MEASURE.segment),
35
+ children: value
36
+ }, `s-${segIndex}`));
37
+ if (isCollapsedBoundary) items.push(/*#__PURE__*/ jsx(ParameterPathJoint, {}, "ellipsis-joint-pre"), /*#__PURE__*/ jsx(ParameterPathEllipsis, {}, "ellipsis"));
38
+ });
39
+ if (encoding) items.push(/*#__PURE__*/ jsx(ParameterPathJoint, {
40
+ "data-measure": measure(MEASURE.joint)
41
+ }, "enc-joint"), /*#__PURE__*/ jsx(ParameterPathEncoding, {
42
+ "data-measure": measure(MEASURE.encoding),
43
+ children: encoding
44
+ }, "enc"));
45
+ if (forMeasurement) return /*#__PURE__*/ jsx("div", {
46
+ ref: ref,
47
+ "data-slot": "parameter-path-row",
48
+ "data-row": ROW.measure,
49
+ "aria-hidden": "true",
50
+ className: "flex items-center gap-0 absolute left-[-9999px] top-0 invisible pointer-events-none",
51
+ children: /*#__PURE__*/ jsx(TestIdProvider, {
52
+ value: void 0,
53
+ children: items
54
+ })
55
+ });
56
+ return /*#__PURE__*/ jsx("div", {
57
+ ref: ref,
58
+ "data-slot": "parameter-path-row",
59
+ "data-row": ROW.visible,
60
+ className: rowVariants({
61
+ expanded: isExpanded
62
+ }),
63
+ children: items
64
+ });
65
+ };
66
+ ParameterPathRow.displayName = 'ParameterPathRow';
67
+ export { ParameterPathRow };
@@ -1,3 +1,6 @@
1
+ export declare const rowVariants: (props?: ({
2
+ expanded?: boolean | null | undefined;
3
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
1
4
  export declare const segmentVariants: (props?: ({
2
5
  variant?: "default" | "highlighted" | null | undefined;
3
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
@@ -1,4 +1,15 @@
1
1
  import { cva } from "class-variance-authority";
2
+ const rowVariants = cva('flex items-center gap-0 min-w-0', {
3
+ variants: {
4
+ expanded: {
5
+ true: 'flex-wrap',
6
+ false: 'overflow-hidden'
7
+ }
8
+ },
9
+ defaultVariants: {
10
+ expanded: false
11
+ }
12
+ });
2
13
  const segmentVariants = cva('flex items-center justify-center gap-2 shrink-0 text-sm leading-5 whitespace-nowrap font-sans', {
3
14
  variants: {
4
15
  variant: {
@@ -22,4 +33,4 @@ const ellipsisVariants = cva([
22
33
  'h-20 px-2 gap-2',
23
34
  'text-text-secondary'
24
35
  ]);
25
- export { ellipsisVariants, encodingVariants, jointVariants, segmentVariants };
36
+ export { ellipsisVariants, encodingVariants, jointVariants, rowVariants, segmentVariants };
@@ -0,0 +1,11 @@
1
+ export declare const MEASURE: {
2
+ readonly method: "method";
3
+ readonly joint: "joint";
4
+ readonly segment: "segment";
5
+ readonly encoding: "encoding";
6
+ };
7
+ export declare const ROW: {
8
+ readonly visible: "visible";
9
+ readonly measure: "measure";
10
+ };
11
+ export declare const EXPAND_KEYS: readonly string[];
@@ -0,0 +1,15 @@
1
+ const MEASURE = {
2
+ method: 'method',
3
+ joint: 'joint',
4
+ segment: 'segment',
5
+ encoding: 'encoding'
6
+ };
7
+ const ROW = {
8
+ visible: 'visible',
9
+ measure: 'measure'
10
+ };
11
+ const EXPAND_KEYS = [
12
+ 'Enter',
13
+ ' '
14
+ ];
15
+ export { EXPAND_KEYS, MEASURE, ROW };
@@ -12,5 +12,31 @@ export interface ParameterPathProps extends Omit<HTMLAttributes<HTMLDivElement>,
12
12
  segments: string[];
13
13
  encoding?: string;
14
14
  attack?: boolean;
15
+ /**
16
+ * Opt in to click-to-expand. When `true`, a *truncated* path becomes
17
+ * interactive: clicking the `…` collapse indicator (or the path) expands it
18
+ * inline to show every segment, and clicking again collapses it back to
19
+ * `first … last`. No effect when the path already fits without truncation.
20
+ *
21
+ * `expandable` only controls the affordance (whether the user can toggle);
22
+ * the open/closed state is owned by `expanded` / `defaultExpanded`.
23
+ */
24
+ expandable?: boolean;
25
+ /**
26
+ * Controlled expanded state. When provided, the component does not manage its
27
+ * own state — pair it with {@link onExpandedChange}.
28
+ */
29
+ expanded?: boolean;
30
+ /**
31
+ * Initial expanded state for the uncontrolled case. Ignored when `expanded`
32
+ * is provided. Defaults to `false`.
33
+ */
34
+ defaultExpanded?: boolean;
35
+ /**
36
+ * Called with the next expanded state whenever the user toggles the path.
37
+ * Required for controlled usage; also useful for analytics in the
38
+ * uncontrolled case.
39
+ */
40
+ onExpandedChange?: (expanded: boolean) => void;
15
41
  copyFormat?: (data: CopyFormatData) => string;
16
42
  }
@@ -1,10 +1,10 @@
1
1
  import { useLayoutEffect, useState } from "react";
2
+ import { range } from "../../utils/range.js";
2
3
  import { useContainerWidth } from "../Table/lib/useContainerWidth.js";
4
+ import { MEASURE } from "./constants.js";
3
5
  const computeTruncation = ({ containerWidth, methodWidth, encodingWidth, segmentWidths, jointsWidth })=>{
4
6
  const segCount = segmentWidths.length;
5
- const allIndices = Array.from({
6
- length: segCount
7
- }, (_, i)=>i);
7
+ const allIndices = range(segCount);
8
8
  if (segCount <= 2 || containerWidth <= 0) return {
9
9
  isTruncated: false,
10
10
  visibleSegmentIndices: allIndices
@@ -27,17 +27,15 @@ const useParameterPathTruncation = ({ containerRef, measurementRef, segmentCount
27
27
  const containerWidth = useContainerWidth(containerRef);
28
28
  const [result, setResult] = useState({
29
29
  isTruncated: false,
30
- visibleSegmentIndices: Array.from({
31
- length: segmentCount
32
- }, (_, i)=>i)
30
+ visibleSegmentIndices: range(segmentCount)
33
31
  });
34
32
  useLayoutEffect(()=>{
35
33
  const root = measurementRef.current;
36
34
  if (!root) return;
37
- const methodEl = root.querySelector('[data-measure="method"]');
38
- const encodingEl = root.querySelector('[data-measure="encoding"]');
39
- const jointEls = Array.from(root.querySelectorAll('[data-measure="joint"]'));
40
- const segmentEls = Array.from(root.querySelectorAll('[data-measure="segment"]'));
35
+ const methodEl = root.querySelector(`[data-measure="${MEASURE.method}"]`);
36
+ const encodingEl = root.querySelector(`[data-measure="${MEASURE.encoding}"]`);
37
+ const jointEls = Array.from(root.querySelectorAll(`[data-measure="${MEASURE.joint}"]`));
38
+ const segmentEls = Array.from(root.querySelectorAll(`[data-measure="${MEASURE.segment}"]`));
41
39
  const segmentWidths = segmentEls.map((el)=>el.getBoundingClientRect().width);
42
40
  const methodWidth = hasMethod && methodEl ? methodEl.getBoundingClientRect().width : 0;
43
41
  const encodingWidth = hasEncoding && encodingEl ? encodingEl.getBoundingClientRect().width : 0;
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.50.1",
3
- "generatedAt": "2026-06-01T09:06:56.612Z",
2
+ "version": "0.50.2",
3
+ "generatedAt": "2026-06-01T16:00:25.004Z",
4
4
  "components": [
5
5
  {
6
6
  "name": "Accordion",
@@ -32167,6 +32167,26 @@
32167
32167
  "required": false,
32168
32168
  "defaultValue": "false"
32169
32169
  },
32170
+ {
32171
+ "name": "expandable",
32172
+ "type": "boolean | undefined",
32173
+ "required": false,
32174
+ "description": "Opt in to click-to-expand. When `true`, a *truncated* path becomes\ninteractive: clicking the `…` collapse indicator (or the path) expands it\ninline to show every segment, and clicking again collapses it back to\n`first … last`. No effect when the path already fits without truncation.\n\n`expandable` only controls the affordance (whether the user can toggle);\nthe open/closed state is owned by `expanded` / `defaultExpanded`.",
32175
+ "defaultValue": "false"
32176
+ },
32177
+ {
32178
+ "name": "expanded",
32179
+ "type": "boolean | undefined",
32180
+ "required": false,
32181
+ "description": "Controlled expanded state. When provided, the component does not manage its\nown state — pair it with {@link onExpandedChange}."
32182
+ },
32183
+ {
32184
+ "name": "defaultExpanded",
32185
+ "type": "boolean | undefined",
32186
+ "required": false,
32187
+ "description": "Initial expanded state for the uncontrolled case. Ignored when `expanded`\nis provided. Defaults to `false`.",
32188
+ "defaultValue": "false"
32189
+ },
32170
32190
  {
32171
32191
  "name": "copyFormat",
32172
32192
  "type": "((data: CopyFormatData) => string) | undefined",
@@ -32445,6 +32465,14 @@
32445
32465
  }
32446
32466
  ],
32447
32467
  "variants": [
32468
+ {
32469
+ "name": "expanded",
32470
+ "options": [
32471
+ "true",
32472
+ "false"
32473
+ ],
32474
+ "defaultValue": "false"
32475
+ },
32448
32476
  {
32449
32477
  "name": "variant",
32450
32478
  "options": [
@@ -32484,6 +32512,10 @@
32484
32512
  "name": "NoMethod",
32485
32513
  "code": "() => (\n <ParameterPath segments={['cookie', 'session_id']} attack />\n)"
32486
32514
  },
32515
+ {
32516
+ "name": "ExpandableTruncated",
32517
+ "code": "() => (\n <div style={{ width: 720, display: 'flex', justifyContent: 'center' }}>\n <ParameterPath\n method='POST'\n segments={['multipart', 'json_abc', 'json_doc', 'qwerty_doc', 'hash', 'formData', 'get']}\n attack\n expandable\n />\n </div>\n)"
32518
+ },
32487
32519
  {
32488
32520
  "name": "Playground",
32489
32521
  "code": "args => <ParameterPath {...args} />"
@@ -0,0 +1 @@
1
+ export declare const hasTextSelection: () => boolean;
@@ -0,0 +1,2 @@
1
+ const hasTextSelection = ()=>(window.getSelection()?.toString().length ?? 0) > 0;
2
+ export { hasTextSelection };
@@ -0,0 +1 @@
1
+ export declare const range: (length: number) => number[];
@@ -0,0 +1,4 @@
1
+ const range = (length)=>Array.from({
2
+ length: Math.max(0, length)
3
+ }, (_, i)=>i);
4
+ export { range };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wallarm-org/design-system",
3
- "version": "0.50.2",
3
+ "version": "0.51.0",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",