From 9c4008c15e999ffcee604bc1e32d6277e57ea018 Mon Sep 17 00:00:00 2001 From: Jules Exel Date: Thu, 2 Apr 2026 16:21:51 -0400 Subject: [PATCH 1/3] Update select option dropdown --- local.sh | 2 +- package.json | 2 +- .../inputs/select/Select.stories.tsx | 52 +++++- stories/molecules/inputs/select/Select.tsx | 170 +++++++++++++----- tailwind.config.js | 78 ++++++++ 5 files changed, 257 insertions(+), 47 deletions(-) diff --git a/local.sh b/local.sh index 39481e38..c73d18b5 100644 --- a/local.sh +++ b/local.sh @@ -9,7 +9,7 @@ RESET='\033[0m' DIR="$PWD" # The required yarn version to use this script -REQUIRED_YARN_VERSION="1.22.19" +REQUIRED_YARN_VERSION="1.22.22" # Get the installed Yarn version INSTALLED_YARN_VERSION=$(yarn --version 2>/dev/null) diff --git a/package.json b/package.json index 8d3f8c0b..e03aef2c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agility/plenum-ui", - "version": "2.2.8", + "version": "2.3.0", "license": "MIT", "main": "dist/index.js", "module": "dist/index.js", diff --git a/stories/molecules/inputs/select/Select.stories.tsx b/stories/molecules/inputs/select/Select.stories.tsx index f8e7cdd4..f0a01fdd 100644 --- a/stories/molecules/inputs/select/Select.stories.tsx +++ b/stories/molecules/inputs/select/Select.stories.tsx @@ -10,12 +10,16 @@ const meta: Meta = { (Story, context) => { if (context.name === "Default Select Dark BG") { return ( -
+
); } - return ; + return ( +
+ +
+ ); } ] }; @@ -23,13 +27,41 @@ const meta: Meta = { export default meta; type TStory = StoryObj; +const manyCountries = [ + { + label: "Australia", + value: "au", + description: "A country and continent" + }, + { label: "Brazil", value: "br" }, + { label: "Canada", value: "ca" }, + { label: "China", value: "cn" }, + { label: "Denmark", value: "dk" }, + { label: "Egypt", value: "eg" }, + { label: "France", value: "fr" }, + { label: "Germany", value: "de" }, + { label: "India", value: "in" }, + { label: "Italy", value: "it" }, + { label: "Japan", value: "jp" }, + { label: "Mexico", value: "mx" }, + { label: "Netherlands", value: "nl" }, + { label: "New Zealand", value: "nz" }, + { label: "Norway", value: "no" }, + { label: "Portugal", value: "pt" }, + { label: "South Korea", value: "kr" }, + { label: "Spain", value: "es" }, + { label: "Sweden", value: "se" }, + { label: "United Kingdom", value: "gb" }, + { label: "United States", value: "us" } +]; + export const DefaultSelect: TStory = { args: { label: "Label", id: "select", name: "select", options: [ - { label: "Canada", value: "value1" }, + { label: "Canada", value: "value1", description: "A description for Canada." }, { label: "USA", value: "value2" } ], isDisabled: false, @@ -38,6 +70,20 @@ export const DefaultSelect: TStory = { message: "Message" } }; + +export const ManyOptions: TStory = { + args: { + label: "Country", + id: "select-many", + name: "select-many", + options: manyCountries, + isDisabled: false, + isError: false, + isRequired: false, + message: "Scroll to see all options" + } +}; + export const DefaultSelectDarkBG: TStory = { args: { label: "Label", diff --git a/stories/molecules/inputs/select/Select.tsx b/stories/molecules/inputs/select/Select.tsx index c140315f..670ce38f 100644 --- a/stories/molecules/inputs/select/Select.tsx +++ b/stories/molecules/inputs/select/Select.tsx @@ -1,13 +1,24 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useLayoutEffect, useRef, useState } from "react"; import InputLabel from "@/stories/molecules/inputs/InputLabel"; +import { DynamicIcon } from "@/stories/atoms/icons/DynamicIcon"; import { useId } from "@/utils/useId"; import { default as cn } from "classnames"; -import Paragraph from "@/stories/atoms/Typography/Paragraph/Paragraph"; +import { + Combobox as HeadlessCombobox, + ComboboxInput, + ComboboxButton, + ComboboxOptions, + ComboboxOption +} from "@headlessui/react"; +import { Paragraph } from "@/stories/atoms/Typography/Paragraph"; export interface ISimpleSelectOptions { label: string; value: string; + emoji?: string; + description?: string; } + export interface ISelectProps { /** Label */ label?: string; @@ -17,7 +28,7 @@ export interface ISelectProps { name?: string; /** List of options to display in the select menu */ options: ISimpleSelectOptions[]; - /** Select name prop */ + /** Called with the selected option's value string */ onChange?(value: string): void; /** Select disabled state */ isDisabled?: boolean; @@ -30,7 +41,11 @@ export interface ISelectProps { onFocus?: () => void; onBlur?: () => void; message?: string; + inputRef?: React.RefObject; + placeholder?: string; + dropdownMaxHeight?: number; } + const Select: React.FC = ({ label, id, @@ -44,57 +59,128 @@ const Select: React.FC = ({ className, onFocus, onBlur, - message + message, + inputRef, + placeholder = "Select", + dropdownMaxHeight = 240 }) => { - const [selectedOption, setSelectedOption] = useState(value || options[0].value); const uniqueID = useId(); if (!id) id = `select-${uniqueID}`; if (!name) name = id; + const findOption = (val?: string) => options.find((o) => o.value === val) ?? null; + + const [selectedOption, setSelectedOption] = useState(findOption(value)); + useEffect(() => { - if (value !== undefined && value !== null) { - setSelectedOption(value); - } + setSelectedOption(findOption(value)); }, [value]); - const handleChange = (e: React.ChangeEvent) => { - const targetValue = e.target.value; - typeof onChange == "function" && onChange(targetValue); - setSelectedOption(targetValue); + const handleChange = (option: ISimpleSelectOptions | null) => { + setSelectedOption(option); + if (option && typeof onChange === "function") { + onChange(option.value); + } }; - const wrapperStyle = cn("group", { "opacity-50": isDisabled }); + + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(); + + useLayoutEffect(() => { + const el = containerRef.current; + if (!el) return; + const observer = new ResizeObserver(([entry]) => setContainerWidth(entry.contentRect.width)); + observer.observe(el); + return () => observer.disconnect(); + }, []); + + const wrapperStyle = cn(className, "w-full", "group", { "opacity-50 pointer-events-none": isDisabled }); + return (
- {label && } - - {message && ( - - {message} - - )} +
); }; diff --git a/tailwind.config.js b/tailwind.config.js index ae41375d..a46bccb6 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -52,6 +52,72 @@ module.exports = { header: "max-content 1fr 1fr" }, colors: { + "neutral-50": "#F7F7F7", + "neutral-100": "#F2F2F2", + "neutral-200": "#E5E7EB", + "neutral-300": "#D1D5DB", + "neutral-400": "#9CA3aF", + "neutral-500": "#6B7280", + "neutral-600": "#4B5563", + "neutral-700": "#374151", + "neutral-800": "#1F2937", + "neutral-900": "#111827", + + "primary-50": "#F7F7F7", + "primary-100": "#EDE9FE", + "primary-200": "#DDD6FE", + "primary-300": "#C4B5FD", + "primary-400": "#A78BFA", + "primary-500": "#8B5CF6", + "primary-600": "#7C3AED", + "primary-700": "#6D28D9", + "primary-800": "#5B21B6", + "primary-900": "#4C1D95", + + "secondary-50": "#FFFAEA", + "secondary-100": "#FFF5D4", + "secondary-200": "#FFEAA9", + "secondary-300": "#FFE07E", + "secondary-400": "#FFD553", + "secondary-500": "#FFCB28", + "secondary-600": "#F2C126", + "secondary-700": "#D9AD22", + "secondary-800": "#BF981E", + "secondary-900": "#997A18", + + "success-50": "#ECFDF5", + "success-100": "#D1FAE5", + "success-200": "#A7F3D0", + "success-300": "#6EE7B7", + "success-400": "#34D399", + "success-500": "#10B981", + "success-600": "#059669", + "success-700": "#047857", + "success-800": "#065F46", + "success-900": "#064E3B", + + "warning-50": "#FFF7ED", + "warning-100": "#FFEDD5", + "warning-200": "#FED7AA", + "warning-300": "#FDBA74", + "warning-400": "#FB923C", + "warning-500": "#F97316", + "warning-600": "#EA580C", + "warning-700": "#C2410C", + "warning-800": "#9A3412", + "warning-900": "#7C2D12", + + "error-50": "#FEF2F2", + "error-100": "#FEE2E2", + "error-200": "#FECACA", + "error-300": "#FCA5A5", + "error-400": "#F87171", + "error-500": "#EF4444", + "error-600": "#DC2626", + "error-700": "#B91C1C", + "error-800": "#991B1B", + "error-900": "#7F1D1D", + "transparent-white-05": "rgba(255, 255, 255, 0.05)", "transparent-white-10": "rgba(255, 255, 255, 0.1)", "transparent-white-20": "rgba(255, 255, 255, 0.2)", @@ -287,6 +353,18 @@ module.exports = { transitionProperty: { left: "left", height: "height" + }, + spacing: { + xxsm: "4px", + xsm: "8px", + sm: "12px", + md: "16px", + lg: "20px", + xlg: "24px", + xxlg: "28px", + hg: "32px", + xhg: "40px", + xxhg: "80px" } } }, From 6e7a22610d91115ee295b6937e193564358ea7da Mon Sep 17 00:00:00 2001 From: Jules Exel Date: Mon, 6 Apr 2026 11:12:35 -0400 Subject: [PATCH 2/3] Update Select.tsx update proper message spacing --- stories/molecules/inputs/select/Select.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stories/molecules/inputs/select/Select.tsx b/stories/molecules/inputs/select/Select.tsx index 670ce38f..4fcc0897 100644 --- a/stories/molecules/inputs/select/Select.tsx +++ b/stories/molecules/inputs/select/Select.tsx @@ -176,7 +176,7 @@ const Select: React.FC = ({
{message && ( - + {message} )} From 63b575d6c86bdf8f3fb6727f35d29d18bd29b41a Mon Sep 17 00:00:00 2001 From: Jules Exel Date: Wed, 8 Apr 2026 09:13:05 -0400 Subject: [PATCH 3/3] update typography --- stories/molecules/inputs/select/Select.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stories/molecules/inputs/select/Select.tsx b/stories/molecules/inputs/select/Select.tsx index 4fcc0897..2047d893 100644 --- a/stories/molecules/inputs/select/Select.tsx +++ b/stories/molecules/inputs/select/Select.tsx @@ -164,9 +164,9 @@ const Select: React.FC = ({ > {({ selected }) => (
- {option.label} + {option.label} {option.description ? ( - {option.description} + {option.description} ) : null}
)}