@wallarm-org/design-system 1.3.0 → 1.4.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.
@@ -0,0 +1,32 @@
1
+ import type { FC, HTMLAttributes, Ref } from 'react';
2
+ import type { TestableProps } from '../../utils/testId';
3
+ type FormatNumberNotation = 'compact' | 'standard';
4
+ type FormatNumberType = 'decimal' | 'percent' | 'byte';
5
+ interface FormatNumberBaseProps {
6
+ /** Numeric value to display. null/undefined renders em dash + "No data" tooltip. */
7
+ value: number | null | undefined;
8
+ /**
9
+ * Formatting category. Default: 'decimal'.
10
+ * - 'decimal': plain number with optional abbreviation
11
+ * - 'percent': percentage with boundary safety
12
+ * - 'byte': decimal byte units: 512 B, 2.3 GB
13
+ */
14
+ type?: FormatNumberType;
15
+ /**
16
+ * 'compact' abbreviates (12k, 59.6M); 'standard' shows full value.
17
+ * Default: 'compact'. Only applies to type='decimal' and type='byte'.
18
+ */
19
+ notation?: FormatNumberNotation;
20
+ /**
21
+ * Unit label for accessible name + tooltip: "requests", "errors".
22
+ * Appended after a space. Only meaningful for type='decimal'.
23
+ */
24
+ unit?: string;
25
+ /** Decimal places for percent. Default: 0. */
26
+ decimals?: number;
27
+ ref?: Ref<HTMLSpanElement>;
28
+ }
29
+ type FormatNumberNativeProps = Omit<HTMLAttributes<HTMLSpanElement>, 'className'>;
30
+ export type FormatNumberProps = FormatNumberNativeProps & FormatNumberBaseProps & TestableProps;
31
+ export declare const FormatNumber: FC<FormatNumberProps>;
32
+ export {};
@@ -0,0 +1,122 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { abbreviateNumber, formatBytes, formatFullNumber, formatPercent } from "../../utils/abbreviateNumber.js";
3
+ import { cn } from "../../utils/cn.js";
4
+ import { Text } from "../Text/index.js";
5
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../Tooltip/index.js";
6
+ const DASHED_UNDERLINE = 'border-b-1 border-dashed border-border-strong-primary';
7
+ const FormatNumber = ({ value, type = 'decimal', notation = 'compact', unit, decimals = 0, ref, ...props })=>{
8
+ if (null == value) return /*#__PURE__*/ jsxs(Tooltip, {
9
+ children: [
10
+ /*#__PURE__*/ jsx(TooltipTrigger, {
11
+ asChild: true,
12
+ children: /*#__PURE__*/ jsx("span", {
13
+ ref: ref,
14
+ "data-slot": "format-number",
15
+ ...props,
16
+ children: /*#__PURE__*/ jsx(Text, {
17
+ size: "sm",
18
+ color: "secondary",
19
+ children: "—"
20
+ })
21
+ })
22
+ }),
23
+ /*#__PURE__*/ jsx(TooltipContent, {
24
+ children: "No data"
25
+ })
26
+ ]
27
+ });
28
+ if (!Number.isFinite(value)) return /*#__PURE__*/ jsx("span", {
29
+ ref: ref,
30
+ "data-slot": "format-number",
31
+ ...props,
32
+ children: /*#__PURE__*/ jsx(Text, {
33
+ size: "sm",
34
+ color: "secondary",
35
+ children: "—"
36
+ })
37
+ });
38
+ if ('percent' === type) return /*#__PURE__*/ jsx("span", {
39
+ ref: ref,
40
+ "data-slot": "format-number",
41
+ ...props,
42
+ children: /*#__PURE__*/ jsx(Text, {
43
+ size: "sm",
44
+ children: formatPercent(value, decimals)
45
+ })
46
+ });
47
+ if ('byte' === type) {
48
+ const { display, full } = formatBytes(value);
49
+ const isAbbreviated = 'compact' === notation && value >= 1000;
50
+ if (isAbbreviated) return /*#__PURE__*/ jsxs(Tooltip, {
51
+ children: [
52
+ /*#__PURE__*/ jsx(TooltipTrigger, {
53
+ asChild: true,
54
+ children: /*#__PURE__*/ jsx("span", {
55
+ ref: ref,
56
+ "data-slot": "format-number",
57
+ "aria-label": full,
58
+ ...props,
59
+ children: /*#__PURE__*/ jsx(Text, {
60
+ size: "sm",
61
+ children: /*#__PURE__*/ jsx("span", {
62
+ className: cn('whitespace-nowrap tabular-nums', DASHED_UNDERLINE),
63
+ children: display
64
+ })
65
+ })
66
+ })
67
+ }),
68
+ /*#__PURE__*/ jsx(TooltipContent, {
69
+ children: full
70
+ })
71
+ ]
72
+ });
73
+ return /*#__PURE__*/ jsx("span", {
74
+ ref: ref,
75
+ "data-slot": "format-number",
76
+ ...props,
77
+ children: /*#__PURE__*/ jsx(Text, {
78
+ size: "sm",
79
+ children: full
80
+ })
81
+ });
82
+ }
83
+ const abbreviated = abbreviateNumber(value);
84
+ const fullValue = formatFullNumber(value);
85
+ const isAbbreviated = 'compact' === notation && Math.abs(value) >= 1000;
86
+ const fullWithUnit = unit ? `${fullValue}\u00A0${unit}` : fullValue;
87
+ if (isAbbreviated) return /*#__PURE__*/ jsxs(Tooltip, {
88
+ children: [
89
+ /*#__PURE__*/ jsx(TooltipTrigger, {
90
+ asChild: true,
91
+ children: /*#__PURE__*/ jsx("span", {
92
+ ref: ref,
93
+ "data-slot": "format-number",
94
+ "aria-label": fullWithUnit,
95
+ ...props,
96
+ children: /*#__PURE__*/ jsx(Text, {
97
+ size: "sm",
98
+ children: /*#__PURE__*/ jsx("span", {
99
+ className: cn('whitespace-nowrap tabular-nums', DASHED_UNDERLINE),
100
+ children: abbreviated
101
+ })
102
+ })
103
+ })
104
+ }),
105
+ /*#__PURE__*/ jsx(TooltipContent, {
106
+ children: fullWithUnit
107
+ })
108
+ ]
109
+ });
110
+ const displayValue = unit ? `${fullValue}\u00A0${unit}` : fullValue;
111
+ return /*#__PURE__*/ jsx("span", {
112
+ ref: ref,
113
+ "data-slot": "format-number",
114
+ ...props,
115
+ children: /*#__PURE__*/ jsx(Text, {
116
+ size: "sm",
117
+ children: displayValue
118
+ })
119
+ });
120
+ };
121
+ FormatNumber.displayName = 'FormatNumber';
122
+ export { FormatNumber };
@@ -0,0 +1 @@
1
+ export { FormatNumber, type FormatNumberProps } from './FormatNumber';
@@ -0,0 +1 @@
1
+ export { FormatNumber } from "./FormatNumber.js";
package/dist/index.d.ts CHANGED
@@ -36,6 +36,7 @@ export { Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLab
36
36
  export { type Condition, type ExprNode, type FieldMetadata, type FieldType, FilterInput, FilterInputChip, type FilterInputChipData, type FilterInputChipProps, type FilterInputChipVariant, FilterInputFieldMenu, type FilterInputFieldMenuProps, FilterInputOperatorMenu, type FilterInputOperatorMenuProps, type FilterInputProps, type FilterOperator, type Group, } from './components/FilterInput';
37
37
  export { Flex, type FlexProps } from './components/Flex';
38
38
  export { FormatDateTime, type FormatDateTimeProps } from './components/FormatDateTime';
39
+ export { FormatNumber, type FormatNumberProps } from './components/FormatNumber';
39
40
  export { Heading, type HeadingProps } from './components/Heading';
40
41
  export { HTTP_METHOD_COLOR, HTTP_METHODS, HttpMethod, type HttpMethodName, type HttpMethodProps, } from './components/HttpMethod';
41
42
  export { Indicator, type IndicatorProps } from './components/Indicator';
package/dist/index.js CHANGED
@@ -25,6 +25,7 @@ export { Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLab
25
25
  export { FilterInput, FilterInputChip, FilterInputFieldMenu, FilterInputOperatorMenu } from "./components/FilterInput/index.js";
26
26
  export { Flex } from "./components/Flex/index.js";
27
27
  export { FormatDateTime } from "./components/FormatDateTime/index.js";
28
+ export { FormatNumber } from "./components/FormatNumber/index.js";
28
29
  export { Heading } from "./components/Heading/index.js";
29
30
  export { HTTP_METHODS, HTTP_METHOD_COLOR, HttpMethod } from "./components/HttpMethod/index.js";
30
31
  export { Indicator } from "./components/Indicator/index.js";
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.2.0",
3
- "generatedAt": "2026-08-20T20:04:41.723Z",
2
+ "version": "1.3.0",
3
+ "generatedAt": "2026-08-20T21:26:32.176Z",
4
4
  "components": [
5
5
  {
6
6
  "name": "Accordion",
@@ -31504,6 +31504,351 @@
31504
31504
  }
31505
31505
  ]
31506
31506
  },
31507
+ {
31508
+ "name": "FormatNumber",
31509
+ "importPath": "@wallarm-org/design-system/FormatNumber",
31510
+ "props": [
31511
+ {
31512
+ "name": "defaultChecked",
31513
+ "type": "boolean | undefined",
31514
+ "required": false
31515
+ },
31516
+ {
31517
+ "name": "defaultValue",
31518
+ "type": "string | number | readonly string[] | undefined",
31519
+ "required": false
31520
+ },
31521
+ {
31522
+ "name": "suppressContentEditableWarning",
31523
+ "type": "boolean | undefined",
31524
+ "required": false
31525
+ },
31526
+ {
31527
+ "name": "suppressHydrationWarning",
31528
+ "type": "boolean | undefined",
31529
+ "required": false
31530
+ },
31531
+ {
31532
+ "name": "accessKey",
31533
+ "type": "string | undefined",
31534
+ "required": false
31535
+ },
31536
+ {
31537
+ "name": "autoCapitalize",
31538
+ "type": "\"off\" | \"none\" | \"on\" | \"sentences\" | \"words\" | \"characters\" | (string & {}) | undefined",
31539
+ "required": false
31540
+ },
31541
+ {
31542
+ "name": "autoFocus",
31543
+ "type": "boolean | undefined",
31544
+ "required": false
31545
+ },
31546
+ {
31547
+ "name": "contentEditable",
31548
+ "type": "Booleanish | \"inherit\" | \"plaintext-only\" | undefined",
31549
+ "required": false
31550
+ },
31551
+ {
31552
+ "name": "contextMenu",
31553
+ "type": "string | undefined",
31554
+ "required": false
31555
+ },
31556
+ {
31557
+ "name": "dir",
31558
+ "type": "string | undefined",
31559
+ "required": false
31560
+ },
31561
+ {
31562
+ "name": "draggable",
31563
+ "type": "Booleanish | undefined",
31564
+ "required": false
31565
+ },
31566
+ {
31567
+ "name": "enterKeyHint",
31568
+ "type": "\"enter\" | \"done\" | \"go\" | \"next\" | \"previous\" | \"search\" | \"send\" | undefined",
31569
+ "required": false
31570
+ },
31571
+ {
31572
+ "name": "hidden",
31573
+ "type": "boolean | undefined",
31574
+ "required": false
31575
+ },
31576
+ {
31577
+ "name": "id",
31578
+ "type": "string | undefined",
31579
+ "required": false
31580
+ },
31581
+ {
31582
+ "name": "lang",
31583
+ "type": "string | undefined",
31584
+ "required": false
31585
+ },
31586
+ {
31587
+ "name": "nonce",
31588
+ "type": "string | undefined",
31589
+ "required": false
31590
+ },
31591
+ {
31592
+ "name": "slot",
31593
+ "type": "string | undefined",
31594
+ "required": false
31595
+ },
31596
+ {
31597
+ "name": "spellCheck",
31598
+ "type": "Booleanish | undefined",
31599
+ "required": false
31600
+ },
31601
+ {
31602
+ "name": "tabIndex",
31603
+ "type": "number | undefined",
31604
+ "required": false
31605
+ },
31606
+ {
31607
+ "name": "title",
31608
+ "type": "string | undefined",
31609
+ "required": false
31610
+ },
31611
+ {
31612
+ "name": "translate",
31613
+ "type": "\"yes\" | \"no\" | undefined",
31614
+ "required": false
31615
+ },
31616
+ {
31617
+ "name": "radioGroup",
31618
+ "type": "string | undefined",
31619
+ "required": false
31620
+ },
31621
+ {
31622
+ "name": "role",
31623
+ "type": "AriaRole | undefined",
31624
+ "required": false
31625
+ },
31626
+ {
31627
+ "name": "about",
31628
+ "type": "string | undefined",
31629
+ "required": false
31630
+ },
31631
+ {
31632
+ "name": "content",
31633
+ "type": "string | undefined",
31634
+ "required": false
31635
+ },
31636
+ {
31637
+ "name": "datatype",
31638
+ "type": "string | undefined",
31639
+ "required": false
31640
+ },
31641
+ {
31642
+ "name": "inlist",
31643
+ "type": "any",
31644
+ "required": false
31645
+ },
31646
+ {
31647
+ "name": "prefix",
31648
+ "type": "string | undefined",
31649
+ "required": false
31650
+ },
31651
+ {
31652
+ "name": "property",
31653
+ "type": "string | undefined",
31654
+ "required": false
31655
+ },
31656
+ {
31657
+ "name": "rel",
31658
+ "type": "string | undefined",
31659
+ "required": false
31660
+ },
31661
+ {
31662
+ "name": "resource",
31663
+ "type": "string | undefined",
31664
+ "required": false
31665
+ },
31666
+ {
31667
+ "name": "rev",
31668
+ "type": "string | undefined",
31669
+ "required": false
31670
+ },
31671
+ {
31672
+ "name": "typeof",
31673
+ "type": "string | undefined",
31674
+ "required": false
31675
+ },
31676
+ {
31677
+ "name": "vocab",
31678
+ "type": "string | undefined",
31679
+ "required": false
31680
+ },
31681
+ {
31682
+ "name": "autoCorrect",
31683
+ "type": "string | undefined",
31684
+ "required": false
31685
+ },
31686
+ {
31687
+ "name": "autoSave",
31688
+ "type": "string | undefined",
31689
+ "required": false
31690
+ },
31691
+ {
31692
+ "name": "color",
31693
+ "type": "string | undefined",
31694
+ "required": false
31695
+ },
31696
+ {
31697
+ "name": "itemProp",
31698
+ "type": "string | undefined",
31699
+ "required": false
31700
+ },
31701
+ {
31702
+ "name": "itemScope",
31703
+ "type": "boolean | undefined",
31704
+ "required": false
31705
+ },
31706
+ {
31707
+ "name": "itemType",
31708
+ "type": "string | undefined",
31709
+ "required": false
31710
+ },
31711
+ {
31712
+ "name": "itemID",
31713
+ "type": "string | undefined",
31714
+ "required": false
31715
+ },
31716
+ {
31717
+ "name": "itemRef",
31718
+ "type": "string | undefined",
31719
+ "required": false
31720
+ },
31721
+ {
31722
+ "name": "results",
31723
+ "type": "number | undefined",
31724
+ "required": false
31725
+ },
31726
+ {
31727
+ "name": "security",
31728
+ "type": "string | undefined",
31729
+ "required": false
31730
+ },
31731
+ {
31732
+ "name": "unselectable",
31733
+ "type": "\"off\" | \"on\" | undefined",
31734
+ "required": false
31735
+ },
31736
+ {
31737
+ "name": "popover",
31738
+ "type": "\"\" | \"auto\" | \"manual\" | \"hint\" | undefined",
31739
+ "required": false
31740
+ },
31741
+ {
31742
+ "name": "popoverTargetAction",
31743
+ "type": "\"toggle\" | \"show\" | \"hide\" | undefined",
31744
+ "required": false
31745
+ },
31746
+ {
31747
+ "name": "popoverTarget",
31748
+ "type": "string | undefined",
31749
+ "required": false
31750
+ },
31751
+ {
31752
+ "name": "inert",
31753
+ "type": "boolean | undefined",
31754
+ "required": false,
31755
+ "description": "@see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/inert}"
31756
+ },
31757
+ {
31758
+ "name": "inputMode",
31759
+ "type": "\"none\" | \"search\" | \"text\" | \"tel\" | \"url\" | \"email\" | \"numeric\" | \"decimal\" | undefined",
31760
+ "required": false,
31761
+ "description": "Hints at the type of data that might be entered by the user while editing the element or its contents"
31762
+ },
31763
+ {
31764
+ "name": "is",
31765
+ "type": "string | undefined",
31766
+ "required": false,
31767
+ "description": "Specify that a standard HTML element should behave like a defined custom built-in element"
31768
+ },
31769
+ {
31770
+ "name": "exportparts",
31771
+ "type": "string | undefined",
31772
+ "required": false,
31773
+ "description": "@see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/exportparts}"
31774
+ },
31775
+ {
31776
+ "name": "part",
31777
+ "type": "string | undefined",
31778
+ "required": false,
31779
+ "description": "@see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/part}"
31780
+ },
31781
+ {
31782
+ "name": "value",
31783
+ "type": "number | null | undefined",
31784
+ "required": true,
31785
+ "description": "Numeric value to display. null/undefined renders em dash + \"No data\" tooltip."
31786
+ },
31787
+ {
31788
+ "name": "type",
31789
+ "type": "FormatNumberType | undefined",
31790
+ "required": false,
31791
+ "description": "Formatting category. Default: 'decimal'.\n- 'decimal': plain number with optional abbreviation\n- 'percent': percentage with boundary safety\n- 'byte': decimal byte units: 512 B, 2.3 GB",
31792
+ "defaultValue": "decimal"
31793
+ },
31794
+ {
31795
+ "name": "notation",
31796
+ "type": "FormatNumberNotation | undefined",
31797
+ "required": false,
31798
+ "description": "'compact' abbreviates (12k, 59.6M); 'standard' shows full value.\nDefault: 'compact'. Only applies to type='decimal' and type='byte'.",
31799
+ "defaultValue": "compact"
31800
+ },
31801
+ {
31802
+ "name": "unit",
31803
+ "type": "string | undefined",
31804
+ "required": false,
31805
+ "description": "Unit label for accessible name + tooltip: \"requests\", \"errors\".\nAppended after a space. Only meaningful for type='decimal'."
31806
+ },
31807
+ {
31808
+ "name": "decimals",
31809
+ "type": "number | undefined",
31810
+ "required": false,
31811
+ "description": "Decimal places for percent. Default: 0.",
31812
+ "defaultValue": "0"
31813
+ }
31814
+ ],
31815
+ "variants": [],
31816
+ "subComponents": [],
31817
+ "examples": [
31818
+ {
31819
+ "name": "Compact",
31820
+ "code": "() => (\n <div className='grid grid-cols-[1fr_1fr] items-center gap-x-16 gap-y-12'>\n {compactValues.map(v => (\n <>\n <Text key={`label-${v}`} size='sm' color='secondary' align='right'>\n {v.toLocaleString('en-US')}\n </Text>\n <FormatNumber key={v} value={v} />\n </>\n ))}\n </div>\n)"
31821
+ },
31822
+ {
31823
+ "name": "Standard",
31824
+ "code": "() => (\n <div className='grid grid-cols-[1fr_1fr] items-center gap-x-16 gap-y-12'>\n {compactValues.map(v => (\n <>\n <Text key={`label-${v}`} size='sm' color='secondary' align='right'>\n {v.toLocaleString('en-US')}\n </Text>\n <FormatNumber key={v} value={v} notation='standard' />\n </>\n ))}\n </div>\n)"
31825
+ },
31826
+ {
31827
+ "name": "WithUnit",
31828
+ "code": "() => (\n <div className='grid grid-cols-[1fr_1fr] items-center gap-x-16 gap-y-12'>\n <Text size='sm' color='secondary' align='right'>\n 42\n </Text>\n <FormatNumber value={42} unit='requests' />\n\n <Text size='sm' color='secondary' align='right'>\n 12,042\n </Text>\n <FormatNumber value={12_042} unit='requests' />\n\n <Text size='sm' color='secondary' align='right'>\n 59,614,283\n </Text>\n <FormatNumber value={59_614_283} unit='errors' />\n\n <Text size='sm' color='secondary' align='right'>\n 500 (standard)\n </Text>\n <FormatNumber value={500} unit='requests' notation='standard' />\n </div>\n)"
31829
+ },
31830
+ {
31831
+ "name": "Percent",
31832
+ "code": "() => (\n <div className='grid grid-cols-[1fr_1fr] items-center gap-x-16 gap-y-12'>\n <Text size='sm' color='secondary' align='right'>\n 0%\n </Text>\n <FormatNumber value={0} type='percent' />\n\n <Text size='sm' color='secondary' align='right'>\n 0.02% (decimals=0)\n </Text>\n <FormatNumber value={0.02} type='percent' />\n\n <Text size='sm' color='secondary' align='right'>\n 0.02% (decimals=1)\n </Text>\n <FormatNumber value={0.02} type='percent' decimals={1} />\n\n <Text size='sm' color='secondary' align='right'>\n 25.5%\n </Text>\n <FormatNumber value={25.5} type='percent' decimals={1} />\n\n <Text size='sm' color='secondary' align='right'>\n 50%\n </Text>\n <FormatNumber value={50} type='percent' />\n\n <Text size='sm' color='secondary' align='right'>\n 99.97% (decimals=0)\n </Text>\n <FormatNumber value={99.97} type='percent' />\n\n <Text size='sm' color='secondary' align='right'>\n 99.97% (decimals=1)\n </Text>\n <FormatNumber value={99.97} type='percent' decimals={1} />\n\n <Text size='sm' color='secondary' align='right'>\n 100%\n </Text>\n <FormatNumber value={100} type='percent' />\n </div>\n)"
31833
+ },
31834
+ {
31835
+ "name": "Bytes",
31836
+ "code": "() => (\n <div className='grid grid-cols-[1fr_1fr] items-center gap-x-16 gap-y-12'>\n <Text size='sm' color='secondary' align='right'>\n 512 (compact)\n </Text>\n <FormatNumber value={512} type='byte' />\n\n <Text size='sm' color='secondary' align='right'>\n 3,400 (compact)\n </Text>\n <FormatNumber value={3_400} type='byte' />\n\n <Text size='sm' color='secondary' align='right'>\n 12,700,000 (compact)\n </Text>\n <FormatNumber value={12_700_000} type='byte' />\n\n <Text size='sm' color='secondary' align='right'>\n 2,345,678,901 (compact)\n </Text>\n <FormatNumber value={2_345_678_901} type='byte' />\n\n <Text size='sm' color='secondary' align='right'>\n 1,100,000,000,000 (compact)\n </Text>\n <FormatNumber value={1_100_000_000_000} type='byte' />\n\n <Text size='sm' color='secondary' align='right'>\n 12,700,000 (standard)\n </Text>\n <FormatNumber value={12_700_000} type='byte' notation='standard' />\n </div>\n)"
31837
+ },
31838
+ {
31839
+ "name": "NegativeValues",
31840
+ "code": "() => (\n <div className='grid grid-cols-[1fr_1fr] items-center gap-x-16 gap-y-12'>\n <Text size='sm' color='secondary' align='right'>\n -42\n </Text>\n <FormatNumber value={-42} />\n\n <Text size='sm' color='secondary' align='right'>\n -12,042\n </Text>\n <FormatNumber value={-12_042} />\n\n <Text size='sm' color='secondary' align='right'>\n -59,614,283\n </Text>\n <FormatNumber value={-59_614_283} />\n </div>\n)"
31841
+ },
31842
+ {
31843
+ "name": "NullValue",
31844
+ "code": "() => (\n <div className='grid grid-cols-[1fr_1fr] items-center gap-x-16 gap-y-12'>\n <Text size='sm' color='secondary' align='right'>\n null\n </Text>\n <FormatNumber value={null} />\n\n <Text size='sm' color='secondary' align='right'>\n undefined\n </Text>\n <FormatNumber value={undefined} />\n </div>\n)"
31845
+ },
31846
+ {
31847
+ "name": "SpecialValues",
31848
+ "code": "() => (\n <div className='grid grid-cols-[1fr_1fr] items-center gap-x-16 gap-y-12'>\n <Text size='sm' color='secondary' align='right'>\n 0\n </Text>\n <FormatNumber value={0} />\n\n <Text size='sm' color='secondary' align='right'>\n NaN\n </Text>\n <FormatNumber value={Number.NaN} />\n\n <Text size='sm' color='secondary' align='right'>\n Infinity\n </Text>\n <FormatNumber value={Number.POSITIVE_INFINITY} />\n </div>\n)"
31849
+ }
31850
+ ]
31851
+ },
31507
31852
  {
31508
31853
  "name": "Heading",
31509
31854
  "importPath": "@wallarm-org/design-system/Heading",
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Abbreviate a number for table cells: `12042` → `"12k"`, `59614283` → `"59.6m"`.
2
+ * Abbreviate a number for table cells: `12042` → `"12k"`, `59614283` → `"59.6M"`.
3
3
  *
4
4
  * Rules (from the Number display & abbreviation guide):
5
- * - Suffixes: k (10³), m (10⁶), b (10⁹), t (10¹²) — lowercase, no space
5
+ * - Suffixes: k (10³), M (10⁶), B (10⁹), T (10¹²) — k lowercase, rest uppercase, no space
6
6
  * - Boundary: moves to next suffix when rounding would reach 1000×
7
7
  * - Precision: ≥100 → 0 decimals; 10–99 → 1 decimal (drop .0); 1–9.9 → 1 decimal
8
8
  * - Negative numbers supported
@@ -12,3 +12,19 @@ export declare const abbreviateNumber: (value: number) => string;
12
12
  * Format a number with thousand separators for tooltips: `59614283` → `"59,614,283"`.
13
13
  */
14
14
  export declare const formatFullNumber: (value: number) => string;
15
+ /**
16
+ * Format a percentage with boundary safety:
17
+ * - Never show 100% when real value < 100 → ">99%" (or ">99.9%" etc.)
18
+ * - Never show 0% when real value > 0 → "<1%" (or "<0.1%" etc.)
19
+ * - `decimals` controls fractional precision (default 0)
20
+ */
21
+ export declare const formatPercent: (value: number, decimals?: number) => string;
22
+ /**
23
+ * Format bytes using decimal (SI) units: 512 B, 3.4 KB, 12.7 MB, 2.3 GB, 1.1 TB.
24
+ * Same decimal precision rules as abbreviateNumber.
25
+ * Returns `{ display, full }` where `full` = "2,345,678,901 bytes".
26
+ */
27
+ export declare const formatBytes: (value: number) => {
28
+ display: string;
29
+ full: string;
30
+ };
@@ -2,17 +2,17 @@ const tiers = [
2
2
  [
3
3
  999500000000,
4
4
  1e12,
5
- 't'
5
+ 'T'
6
6
  ],
7
7
  [
8
8
  999500000,
9
9
  1e9,
10
- 'b'
10
+ 'B'
11
11
  ],
12
12
  [
13
13
  999500,
14
14
  1e6,
15
- 'm'
15
+ 'M'
16
16
  ],
17
17
  [
18
18
  1000,
@@ -36,4 +36,62 @@ const abbreviateNumber = (value)=>{
36
36
  return `${sign}${formatted}${suffix}`;
37
37
  };
38
38
  const formatFullNumber = (value)=>fullFormatter.format(value);
39
- export { abbreviateNumber, formatFullNumber };
39
+ const formatPercent = (value, decimals = 0)=>{
40
+ if (0 === value) return '0%';
41
+ if (100 === value) return '100%';
42
+ const factor = 10 ** decimals;
43
+ const rounded = Math.round(value * factor) / factor;
44
+ if (value > 0 && 0 === rounded) {
45
+ const threshold = 1 / factor;
46
+ return `<${threshold}%`;
47
+ }
48
+ if (value < 100 && 100 === rounded) {
49
+ const max = 100 - 1 / factor;
50
+ return `>${decimals > 0 ? max.toFixed(decimals) : String(max)}%`;
51
+ }
52
+ return `${decimals > 0 ? rounded.toFixed(decimals) : String(rounded)}%`;
53
+ };
54
+ const byteTiers = [
55
+ [
56
+ 999500000000,
57
+ 1e12,
58
+ 'TB'
59
+ ],
60
+ [
61
+ 999500000,
62
+ 1e9,
63
+ 'GB'
64
+ ],
65
+ [
66
+ 999500,
67
+ 1e6,
68
+ 'MB'
69
+ ],
70
+ [
71
+ 1000,
72
+ 1e3,
73
+ 'KB'
74
+ ]
75
+ ];
76
+ const formatBytes = (value)=>{
77
+ const full = `${fullFormatter.format(value)} bytes`;
78
+ if (value < 1000) return {
79
+ display: `${value} B`,
80
+ full
81
+ };
82
+ const tier = byteTiers.find(([threshold])=>value >= threshold);
83
+ if (!tier) return {
84
+ display: `${value} B`,
85
+ full
86
+ };
87
+ const [, divisor, suffix] = tier;
88
+ const scaled = value / divisor;
89
+ const precision = scaled >= 100 ? 0 : 1;
90
+ const rounded = Math.round(scaled * 10 ** precision) / 10 ** precision;
91
+ const formatted = rounded % 1 === 0 ? String(rounded) : rounded.toFixed(1);
92
+ return {
93
+ display: `${formatted} ${suffix}`,
94
+ full
95
+ };
96
+ };
97
+ export { abbreviateNumber, formatBytes, formatFullNumber, formatPercent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wallarm-org/design-system",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",