@agility/plenum-ui 2.2.8 → 2.3.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,13 +1,24 @@
1
- import React, { useEffect, useState } from "react";
1
+ import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
2
2
  import InputLabel from "@/stories/molecules/inputs/InputLabel";
3
+ import { DynamicIcon } from "@/stories/atoms/icons/DynamicIcon";
3
4
  import { useId } from "@/utils/useId";
4
5
  import { default as cn } from "classnames";
5
- import Paragraph from "@/stories/atoms/Typography/Paragraph/Paragraph";
6
+ import {
7
+ Combobox as HeadlessCombobox,
8
+ ComboboxInput,
9
+ ComboboxButton,
10
+ ComboboxOptions,
11
+ ComboboxOption
12
+ } from "@headlessui/react";
13
+ import { Paragraph } from "@/stories/atoms/Typography/Paragraph";
6
14
 
7
15
  export interface ISimpleSelectOptions {
8
16
  label: string;
9
17
  value: string;
18
+ emoji?: string;
19
+ description?: string;
10
20
  }
21
+
11
22
  export interface ISelectProps {
12
23
  /** Label */
13
24
  label?: string;
@@ -17,7 +28,7 @@ export interface ISelectProps {
17
28
  name?: string;
18
29
  /** List of options to display in the select menu */
19
30
  options: ISimpleSelectOptions[];
20
- /** Select name prop */
31
+ /** Called with the selected option's value string */
21
32
  onChange?(value: string): void;
22
33
  /** Select disabled state */
23
34
  isDisabled?: boolean;
@@ -30,7 +41,11 @@ export interface ISelectProps {
30
41
  onFocus?: () => void;
31
42
  onBlur?: () => void;
32
43
  message?: string;
44
+ inputRef?: React.RefObject<HTMLInputElement>;
45
+ placeholder?: string;
46
+ dropdownMaxHeight?: number;
33
47
  }
48
+
34
49
  const Select: React.FC<ISelectProps> = ({
35
50
  label,
36
51
  id,
@@ -44,57 +59,128 @@ const Select: React.FC<ISelectProps> = ({
44
59
  className,
45
60
  onFocus,
46
61
  onBlur,
47
- message
62
+ message,
63
+ inputRef,
64
+ placeholder = "Select",
65
+ dropdownMaxHeight = 240
48
66
  }) => {
49
- const [selectedOption, setSelectedOption] = useState<string>(value || options[0].value);
50
67
  const uniqueID = useId();
51
68
  if (!id) id = `select-${uniqueID}`;
52
69
  if (!name) name = id;
53
70
 
71
+ const findOption = (val?: string) => options.find((o) => o.value === val) ?? null;
72
+
73
+ const [selectedOption, setSelectedOption] = useState<ISimpleSelectOptions | null>(findOption(value));
74
+
54
75
  useEffect(() => {
55
- if (value !== undefined && value !== null) {
56
- setSelectedOption(value);
57
- }
76
+ setSelectedOption(findOption(value));
58
77
  }, [value]);
59
78
 
60
- const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
61
- const targetValue = e.target.value;
62
- typeof onChange == "function" && onChange(targetValue);
63
- setSelectedOption(targetValue);
79
+ const handleChange = (option: ISimpleSelectOptions | null) => {
80
+ setSelectedOption(option);
81
+ if (option && typeof onChange === "function") {
82
+ onChange(option.value);
83
+ }
64
84
  };
65
- const wrapperStyle = cn("group", { "opacity-50": isDisabled });
85
+
86
+ const containerRef = useRef<HTMLDivElement>(null);
87
+ const [containerWidth, setContainerWidth] = useState<number | undefined>();
88
+
89
+ useLayoutEffect(() => {
90
+ const el = containerRef.current;
91
+ if (!el) return;
92
+ const observer = new ResizeObserver(([entry]) => setContainerWidth(entry.contentRect.width));
93
+ observer.observe(el);
94
+ return () => observer.disconnect();
95
+ }, []);
96
+
97
+ const wrapperStyle = cn(className, "w-full", "group", { "opacity-50 pointer-events-none": isDisabled });
98
+
66
99
  return (
67
100
  <div className={wrapperStyle}>
68
- {label && <InputLabel isActive label={label} isRequired={isRequired} id={id} isDisabled={isDisabled} />}
69
- <select
70
- id={id}
71
- name={name}
72
- className={cn(
73
- "block w-full border-gray-300 py-2 pl-3 pr-10 text-base focus:outline-none",
74
- "rounded focus:border-purple-500 focus:ring-purple-500 sm:text-sm",
75
- { "border-red-500": isError },
76
- { "border-gray-300": !isError },
77
- className
101
+ {label && <InputLabel id={`${id}-label`} label={label} isRequired={isRequired} />}
102
+
103
+ <HeadlessCombobox value={selectedOption} onChange={handleChange} disabled={isDisabled} immediate by="value">
104
+ <div ref={containerRef} className="relative w-full">
105
+ <div
106
+ className={cn(
107
+ "relative w-full cursor-default overflow-hidden rounded border bg-white text-left shadow-sm",
108
+ "focus-within:border-primary-800 focus-within:ring-1 focus-within:ring-primary-800",
109
+ { "border-red-500": isError, "border-gray-300": !isError }
110
+ )}
111
+ >
112
+ <ComboboxInput
113
+ id={id}
114
+ name={name}
115
+ ref={inputRef}
116
+ readOnly
117
+ displayValue={(option: ISimpleSelectOptions | null) => (option ? option.label : "")}
118
+ placeholder={placeholder}
119
+ onFocus={onFocus}
120
+ onBlur={onBlur}
121
+ className={cn(
122
+ "w-full border-none py-2 pl-3 pr-10 text-sm leading-5 text-gray-700",
123
+ "placeholder:text-gray-400",
124
+ "focus:outline-none focus:ring-0",
125
+ "bg-transparent cursor-default"
126
+ )}
127
+ />
128
+
129
+ <ComboboxButton className="absolute inset-y-0 right-0 flex items-center pr-3">
130
+ {({ open }) => (
131
+ <DynamicIcon
132
+ icon="IconChevronDown"
133
+ className={cn("h-4 w-4 text-gray-400 transition-transform", { "rotate-180": open })}
134
+ aria-hidden="true"
135
+ />
136
+ )}
137
+ </ComboboxButton>
138
+ </div>
139
+
140
+ <ComboboxOptions
141
+ anchor="bottom start"
142
+ style={
143
+ {
144
+ "--anchor-max-height": `${dropdownMaxHeight}px`,
145
+ minWidth: containerWidth
146
+ } as React.CSSProperties
147
+ }
148
+ className={cn(
149
+ "z-[9999] overflow-auto rounded bg-white py-1",
150
+ "text-sm shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none",
151
+ "[--anchor-gap:8px]"
152
+ )}
153
+ >
154
+ {options.map((option) => (
155
+ <ComboboxOption
156
+ key={option.value}
157
+ value={option}
158
+ className={({ focus }) =>
159
+ cn(
160
+ "relative cursor-default select-none mx-xxsm rounded",
161
+ focus ? "bg-gray-100 text-gray-900" : "text-gray-700"
162
+ )
163
+ }
164
+ >
165
+ {({ selected }) => (
166
+ <div className="py-xxsm px-sm flex items-center gap-xsm">
167
+ <Paragraph size="md">{option.label}</Paragraph>
168
+ {option.description ? (
169
+ <Paragraph size="md" className="text-neutral-500">{option.description}</Paragraph>
170
+ ) : null}
171
+ </div>
172
+ )}
173
+ </ComboboxOption>
174
+ ))}
175
+ </ComboboxOptions>
176
+ </div>
177
+
178
+ {message && (
179
+ <Paragraph size="md" className={isError ? "text-red-600" : "text-gray-500 pt-xxsm"}>
180
+ {message}
181
+ </Paragraph>
78
182
  )}
79
- onChange={handleChange}
80
- disabled={isDisabled}
81
- value={selectedOption}
82
- onFocus={onFocus}
83
- onBlur={onBlur}
84
- >
85
- {options.map(({ value, label }) => {
86
- return (
87
- <option key={value} value={value}>
88
- {label}
89
- </option>
90
- );
91
- })}
92
- </select>
93
- {message && (
94
- <Paragraph size="md" className={isError ? "text-red-600" : "text-gray-500"}>
95
- {message}
96
- </Paragraph>
97
- )}
183
+ </HeadlessCombobox>
98
184
  </div>
99
185
  );
100
186
  };
@@ -52,6 +52,72 @@ module.exports = {
52
52
  header: "max-content 1fr 1fr"
53
53
  },
54
54
  colors: {
55
+ "neutral-50": "#F7F7F7",
56
+ "neutral-100": "#F2F2F2",
57
+ "neutral-200": "#E5E7EB",
58
+ "neutral-300": "#D1D5DB",
59
+ "neutral-400": "#9CA3aF",
60
+ "neutral-500": "#6B7280",
61
+ "neutral-600": "#4B5563",
62
+ "neutral-700": "#374151",
63
+ "neutral-800": "#1F2937",
64
+ "neutral-900": "#111827",
65
+
66
+ "primary-50": "#F7F7F7",
67
+ "primary-100": "#EDE9FE",
68
+ "primary-200": "#DDD6FE",
69
+ "primary-300": "#C4B5FD",
70
+ "primary-400": "#A78BFA",
71
+ "primary-500": "#8B5CF6",
72
+ "primary-600": "#7C3AED",
73
+ "primary-700": "#6D28D9",
74
+ "primary-800": "#5B21B6",
75
+ "primary-900": "#4C1D95",
76
+
77
+ "secondary-50": "#FFFAEA",
78
+ "secondary-100": "#FFF5D4",
79
+ "secondary-200": "#FFEAA9",
80
+ "secondary-300": "#FFE07E",
81
+ "secondary-400": "#FFD553",
82
+ "secondary-500": "#FFCB28",
83
+ "secondary-600": "#F2C126",
84
+ "secondary-700": "#D9AD22",
85
+ "secondary-800": "#BF981E",
86
+ "secondary-900": "#997A18",
87
+
88
+ "success-50": "#ECFDF5",
89
+ "success-100": "#D1FAE5",
90
+ "success-200": "#A7F3D0",
91
+ "success-300": "#6EE7B7",
92
+ "success-400": "#34D399",
93
+ "success-500": "#10B981",
94
+ "success-600": "#059669",
95
+ "success-700": "#047857",
96
+ "success-800": "#065F46",
97
+ "success-900": "#064E3B",
98
+
99
+ "warning-50": "#FFF7ED",
100
+ "warning-100": "#FFEDD5",
101
+ "warning-200": "#FED7AA",
102
+ "warning-300": "#FDBA74",
103
+ "warning-400": "#FB923C",
104
+ "warning-500": "#F97316",
105
+ "warning-600": "#EA580C",
106
+ "warning-700": "#C2410C",
107
+ "warning-800": "#9A3412",
108
+ "warning-900": "#7C2D12",
109
+
110
+ "error-50": "#FEF2F2",
111
+ "error-100": "#FEE2E2",
112
+ "error-200": "#FECACA",
113
+ "error-300": "#FCA5A5",
114
+ "error-400": "#F87171",
115
+ "error-500": "#EF4444",
116
+ "error-600": "#DC2626",
117
+ "error-700": "#B91C1C",
118
+ "error-800": "#991B1B",
119
+ "error-900": "#7F1D1D",
120
+
55
121
  "transparent-white-05": "rgba(255, 255, 255, 0.05)",
56
122
  "transparent-white-10": "rgba(255, 255, 255, 0.1)",
57
123
  "transparent-white-20": "rgba(255, 255, 255, 0.2)",
@@ -287,6 +353,18 @@ module.exports = {
287
353
  transitionProperty: {
288
354
  left: "left",
289
355
  height: "height"
356
+ },
357
+ spacing: {
358
+ xxsm: "4px",
359
+ xsm: "8px",
360
+ sm: "12px",
361
+ md: "16px",
362
+ lg: "20px",
363
+ xlg: "24px",
364
+ xxlg: "28px",
365
+ hg: "32px",
366
+ xhg: "40px",
367
+ xxhg: "80px"
290
368
  }
291
369
  }
292
370
  },
@@ -1,11 +0,0 @@
1
- import React from "react";
2
- export interface IRadialProgressProps extends React.PropsWithChildren {
3
- /** Percentage value to display */
4
- inputValue: number;
5
- /** Radius for the circle - Max value of 100 */
6
- radius: number;
7
- /** Additional classnames */
8
- className?: string;
9
- }
10
- declare const RadialProgress: React.FC<IRadialProgressProps>;
11
- export default RadialProgress;
@@ -1,3 +0,0 @@
1
- import RadialProgress, { IRadialProgressProps } from "./RadialProgress";
2
- export default RadialProgress;
3
- export type { IRadialProgressProps };
@@ -1,19 +0,0 @@
1
- import type { Meta, StoryObj } from "@storybook/react"
2
- import RadialProgress from "./RadialProgress"
3
-
4
- const meta: Meta<typeof RadialProgress> = {
5
- title: "Design System/atoms/Loaders/NProgress/RadialProgress",
6
- component: RadialProgress
7
- }
8
-
9
- type Story = StoryObj<typeof RadialProgress>
10
-
11
- export const DefaultRadialProgress: Story = {
12
- args: {
13
- inputValue: 33,
14
- radius: 20,
15
- children: <></>
16
- }
17
- }
18
-
19
- export default meta
@@ -1,74 +0,0 @@
1
- import React, { useMemo } from "react"
2
- import { default as cn } from "classnames"
3
- export interface IRadialProgressProps extends React.PropsWithChildren {
4
- /** Percentage value to display */
5
- inputValue: number
6
- /** Radius for the circle - Max value of 100 */
7
- radius: number
8
- /** Additional classnames */
9
- className?: string
10
- }
11
-
12
- const RadialProgress: React.FC<IRadialProgressProps> = ({
13
- inputValue,
14
- radius,
15
- children,
16
- className,
17
- }) => {
18
- const r = radius / 2
19
-
20
- if (inputValue < 0) {
21
- inputValue = 0
22
- }
23
- if (inputValue > 100) {
24
- inputValue = 100
25
- }
26
- if (radius < 0) {
27
- radius = 0
28
- }
29
- if (radius > 100) {
30
- radius = 100
31
- }
32
- const drawPercentage = useMemo(() => {
33
- const roundCircum = Math.round(2 * r * Math.PI)
34
- return (inputValue * roundCircum) / 50
35
- }, [inputValue, r])
36
-
37
- const xyPos = (radius + 2) * -1
38
- const viewPortXY = (radius + 2) * 2
39
- return (
40
- <div
41
- className={cn(`overflow-visible`, className && className)}
42
- style={{ height: `${viewPortXY}px`, width: `${viewPortXY}px` }}
43
- >
44
- <svg
45
- viewBox={`${xyPos} ${xyPos} ${viewPortXY} ${viewPortXY}`}
46
- data-percent={drawPercentage}
47
- fill="none"
48
- >
49
- <circle
50
- className="-rotate-90 stroke-gray-200 stroke-1"
51
- cx={0}
52
- cy={0}
53
- r={radius}
54
- ></circle>
55
- <circle
56
- strokeDasharray={`${drawPercentage} 999`}
57
- className="m-1 -rotate-90 stroke-purple-600 stroke-1 transition-all"
58
- cx={0}
59
- cy={0}
60
- r={radius}
61
- ></circle>
62
- </svg>
63
- <div
64
- className={cn(
65
- `h-[${viewPortXY}px] w-[${viewPortXY}px] absolute inset-0 flex items-center justify-center overflow-hidden `
66
- )}
67
- >
68
- {children}
69
- </div>
70
- </div>
71
- )
72
- }
73
-
74
- export default RadialProgress
@@ -1,3 +0,0 @@
1
- import RadialProgress, { IRadialProgressProps } from "./RadialProgress"
2
- export default RadialProgress
3
- export type { IRadialProgressProps }