@grantbii/design-system 2.0.0 → 3.0.3

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 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../core/molecules/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,EACL,OAAO,IAAI,MAAM,EACjB,SAAS,GAGV,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAC","sourcesContent":["export { default as Badge, RawBadge } from \"./Badge\";\nexport {\n default as Button,\n RawButton,\n type ButtonSize,\n type ButtonVariant,\n} from \"./Button\";\nexport { default as Checkbox } from \"./Checkbox\";\nexport { default as Input } from \"./Input\";\nexport { default as Overlay } from \"./Overlay\";\nexport { default as PageLoader } from \"./PageLoader\";\nexport { default as RadioButton } from \"./RadioButton\";\nexport { default as Textarea } from \"./Textarea\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../core/molecules/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACrD,OAAO,EACL,OAAO,IAAI,MAAM,EACjB,SAAS,GAGV,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,SAAS,CAAC;AAC3C,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EACL,OAAO,IAAI,OAAO,EAClB,cAAc,EACd,cAAc,GAKf,MAAM,WAAW,CAAC;AACnB,OAAO,EACL,OAAO,IAAI,KAAK,EAChB,aAAa,EACb,QAAQ,GAIT,MAAM,SAAS,CAAC","sourcesContent":["export { default as Badge, RawBadge } from \"./Badge\";\nexport {\n default as Button,\n RawButton,\n type ButtonSize,\n type ButtonVariant,\n} from \"./Button\";\nexport { default as Checkbox } from \"./Checkbox\";\nexport { default as Input } from \"./Input\";\nexport { default as Overlay } from \"./Overlay\";\nexport { default as PageLoader } from \"./PageLoader\";\nexport { default as RadioButton } from \"./RadioButton\";\nexport { default as Textarea } from \"./Textarea\";\nexport {\n default as Tooltip,\n TooltipContent,\n TooltipTrigger,\n type TooltipContentProps,\n type TooltipProps,\n type TooltipSide,\n type TooltipTriggerProps,\n} from \"./Tooltip\";\nexport {\n default as Toast,\n ToastProvider,\n useToast,\n type ToastOptions,\n type ToastProps,\n type ToastVariant,\n} from \"./Toast\";\n"]}
@@ -1,9 +1,15 @@
1
- import type { ComponentPropsWithoutRef, DetailedHTMLProps, SelectHTMLAttributes } from "react";
2
- import { type Option } from "../types";
3
- type DropdownOption = Option & Omit<ComponentPropsWithoutRef<"option">, keyof Option | "children">;
1
+ import { type CSSProperties } from "react";
2
+ import type { Option } from "../types";
4
3
  export type DropdownProps = {
5
- options: DropdownOption[];
6
- defaultLabel?: string;
7
- } & DetailedHTMLProps<SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement>;
8
- declare const Dropdown: ({ options, defaultLabel, ...selectProps }: DropdownProps) => import("react").JSX.Element;
4
+ options: Option[];
5
+ placeholder?: string;
6
+ value?: string;
7
+ defaultValue?: string;
8
+ onChange?: (value: string) => void;
9
+ disabled?: boolean;
10
+ width?: CSSProperties["width"];
11
+ name?: string;
12
+ form?: string;
13
+ };
14
+ declare const Dropdown: ({ options, placeholder, value, defaultValue, onChange, disabled, width, name, form, }: DropdownProps) => import("react").JSX.Element;
9
15
  export default Dropdown;
@@ -1,21 +1,120 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
2
3
  import styled from "styled-components";
3
- import { Color, Typography } from "../atoms";
4
- import {} from "../types";
5
- const Dropdown = ({ options, defaultLabel, ...selectProps }) => (_jsxs(Select, { ...selectProps, children: [_jsx("option", { hidden: true, disabled: true, value: "", children: defaultLabel ?? "-" }), options.map(({ label, ...optionProps }) => (_jsx("option", { ...optionProps, children: label }, optionProps.value)))] }));
4
+ import { Color, Spacing, SystemIcon, Typography } from "../atoms";
5
+ import { applyTypography } from "../integrations";
6
+ const Dropdown = ({ options, placeholder = "Select an option", value, defaultValue = "", onChange, disabled, width = "100%", name, form, }) => {
7
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
8
+ const [isOpen, setIsOpen] = useState(false);
9
+ const dropdownRef = useRef(null);
10
+ const hiddenInputRef = useRef(null);
11
+ const selectedValue = value ?? uncontrolledValue;
12
+ const selectedOptionIndex = options.findIndex((option) => option.value === selectedValue);
13
+ const initialActiveOptionIndex = Math.max(0, selectedOptionIndex);
14
+ const [activeOptionIndex, setActiveOptionIndex] = useState(initialActiveOptionIndex);
15
+ const triggerButtonRef = useRef(null);
16
+ useEffect(() => {
17
+ if (!isOpen)
18
+ return;
19
+ const close = (event) => {
20
+ if (!dropdownRef.current?.contains(event.target))
21
+ setIsOpen(false);
22
+ };
23
+ document.addEventListener("pointerdown", close);
24
+ return () => document.removeEventListener("pointerdown", close);
25
+ }, [isOpen]);
26
+ useEffect(() => {
27
+ const formElement = hiddenInputRef.current?.form;
28
+ if (!formElement || value !== undefined)
29
+ return;
30
+ const reset = () => {
31
+ setUncontrolledValue(defaultValue);
32
+ setIsOpen(false);
33
+ };
34
+ formElement.addEventListener("reset", reset);
35
+ return () => formElement.removeEventListener("reset", reset);
36
+ }, [defaultValue, value]);
37
+ const openOptions = (index = initialActiveOptionIndex) => {
38
+ if (!options.length)
39
+ return;
40
+ setActiveOptionIndex(index);
41
+ setIsOpen(true);
42
+ };
43
+ const selectOption = (index) => {
44
+ const option = options[index];
45
+ if (!option)
46
+ return;
47
+ if (value === undefined)
48
+ setUncontrolledValue(option.value);
49
+ onChange?.(option.value);
50
+ setIsOpen(false);
51
+ triggerButtonRef.current?.focus();
52
+ };
53
+ return (_jsxs(Container, { ref: dropdownRef, "$width": width, onBlur: (event) => {
54
+ if (!event.currentTarget.contains(event.relatedTarget))
55
+ setIsOpen(false);
56
+ }, children: [_jsxs(Trigger, { ref: triggerButtonRef, "$isOpen": isOpen, type: "button", disabled: disabled, onClick: () => (isOpen ? setIsOpen(false) : openOptions()), children: [_jsx("span", { children: options[selectedOptionIndex]?.label ?? placeholder }), _jsx(CaretDownIcon, { "$isOpen": isOpen })] }), name && (_jsx("input", { ref: hiddenInputRef, type: "hidden", name: name, form: form, value: selectedValue, disabled: disabled })), isOpen && (_jsx(Options, { role: "listbox", tabIndex: -1, autoFocus: true, children: options.map((option, index) => (_jsx(OptionItem, { role: "option", "$isActive": index === activeOptionIndex, onClick: () => selectOption(index), onMouseEnter: () => setActiveOptionIndex(index), children: option.label }, option.value))) }))] }));
57
+ };
6
58
  export default Dropdown;
7
- const Select = styled.select `
8
- padding: 12px 16px;
9
- border-radius: 8px;
59
+ const Container = styled.div `
60
+ position: relative;
61
+ width: ${({ $width }) => typeof $width === "number" ? `${$width}px` : $width};
62
+ `;
63
+ const Trigger = styled.button `
64
+ box-sizing: border-box;
65
+ display: flex;
66
+ align-items: center;
67
+ justify-content: space-between;
68
+ gap: ${Spacing.px12};
69
+ width: 100%;
70
+ padding: ${Spacing.px12} ${Spacing.px16};
71
+ border: 1px solid ${Color.neutral.grey2};
72
+ border-radius: ${Spacing.px8};
10
73
 
11
- font-size: 16px;
12
- font-weight: ${Typography.weight.medium};
74
+ ${applyTypography(Typography.bodyPrimaryMedium)}
13
75
 
14
- background-color: transparent;
15
- border: 1px solid ${Color.neutral.grey2};
76
+ color: ${Color.neutral.black};
77
+ background: ${({ $isOpen }) => $isOpen ? Color.accent.blue3 : Color.neutral.white};
78
+ cursor: pointer;
79
+
80
+ > span {
81
+ min-width: 0;
82
+ overflow: hidden;
83
+ text-overflow: ellipsis;
84
+ white-space: nowrap;
85
+ }
16
86
 
17
- &:active {
18
- background-color: ${Color.accent.blue3};
87
+ &:disabled {
88
+ color: ${Color.typography.blackLow};
89
+ background: ${Color.neutral.grey4};
90
+ cursor: not-allowed;
19
91
  }
20
92
  `;
93
+ const CaretDownIcon = styled(SystemIcon.CaretDownIcon).attrs({ size: 20 }) `
94
+ flex: none;
95
+ transform: rotate(${({ $isOpen }) => ($isOpen ? "180deg" : "0")});
96
+ `;
97
+ const Options = styled.div `
98
+ position: absolute;
99
+ z-index: 1;
100
+ top: calc(100% + ${Spacing.px8});
101
+ left: 0;
102
+ box-sizing: border-box;
103
+ width: 100%;
104
+ max-height: 320px;
105
+ overflow-y: auto;
106
+ padding: ${Spacing.px4} 0;
107
+ border: 1px solid ${Color.neutral.grey3};
108
+ border-radius: ${Spacing.px8};
109
+ background: ${Color.neutral.white};
110
+ `;
111
+ const OptionItem = styled.div `
112
+ padding: 10px ${Spacing.px16};
113
+
114
+ ${applyTypography(Typography.bodyPrimaryRegular)}
115
+
116
+ color: ${Color.neutral.black};
117
+ background: ${({ $isActive }) => $isActive ? Color.accent.yellow3 : Color.neutral.white};
118
+ cursor: pointer;
119
+ `;
21
120
  //# sourceMappingURL=Dropdown.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Dropdown.js","sourceRoot":"","sources":["../../../core/organisms/Dropdown.tsx"],"names":[],"mappings":";AAKA,OAAO,MAAM,MAAM,mBAAmB,CAAC;AACvC,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAe,MAAM,UAAU,CAAC;AAavC,MAAM,QAAQ,GAAG,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,WAAW,EAAiB,EAAE,EAAE,CAAC,CAC7E,MAAC,MAAM,OAAK,WAAW,aACrB,iBAAQ,MAAM,QAAC,QAAQ,QAAC,KAAK,EAAC,EAAE,YAC7B,YAAY,IAAI,GAAG,GACb,EAER,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,GAAG,WAAW,EAAE,EAAE,EAAE,CAAC,CAC1C,oBAAoC,WAAW,YAC5C,KAAK,IADK,WAAW,CAAC,KAAK,CAErB,CACV,CAAC,IACK,CACV,CAAC;AAEF,eAAe,QAAQ,CAAC;AAExB,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAA;;;;;iBAKX,UAAU,CAAC,MAAM,CAAC,MAAM;;;sBAGnB,KAAK,CAAC,OAAO,CAAC,KAAK;;;wBAGjB,KAAK,CAAC,MAAM,CAAC,KAAK;;CAEzC,CAAC","sourcesContent":["import type {\n ComponentPropsWithoutRef,\n DetailedHTMLProps,\n SelectHTMLAttributes,\n} from \"react\";\nimport styled from \"styled-components\";\nimport { Color, Typography } from \"../atoms\";\nimport { type Option } from \"../types\";\n\ntype DropdownOption = Option &\n Omit<ComponentPropsWithoutRef<\"option\">, keyof Option | \"children\">;\n\nexport type DropdownProps = {\n options: DropdownOption[];\n defaultLabel?: string;\n} & DetailedHTMLProps<\n SelectHTMLAttributes<HTMLSelectElement>,\n HTMLSelectElement\n>;\n\nconst Dropdown = ({ options, defaultLabel, ...selectProps }: DropdownProps) => (\n <Select {...selectProps}>\n <option hidden disabled value=\"\">\n {defaultLabel ?? \"-\"}\n </option>\n\n {options.map(({ label, ...optionProps }) => (\n <option key={optionProps.value} {...optionProps}>\n {label}\n </option>\n ))}\n </Select>\n);\n\nexport default Dropdown;\n\nconst Select = styled.select`\n padding: 12px 16px;\n border-radius: 8px;\n\n font-size: 16px;\n font-weight: ${Typography.weight.medium};\n\n background-color: transparent;\n border: 1px solid ${Color.neutral.grey2};\n\n &:active {\n background-color: ${Color.accent.blue3};\n }\n`;\n"]}
1
+ {"version":3,"file":"Dropdown.js","sourceRoot":"","sources":["../../../core/organisms/Dropdown.tsx"],"names":[],"mappings":"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","sourcesContent":["import { type CSSProperties, useEffect, useRef, useState } from \"react\";\nimport styled from \"styled-components\";\nimport { Color, Spacing, SystemIcon, Typography } from \"../atoms\";\nimport { applyTypography } from \"../integrations\";\nimport type { Option } from \"../types\";\n\nexport type DropdownProps = {\n options: Option[];\n placeholder?: string;\n value?: string;\n defaultValue?: string;\n onChange?: (value: string) => void;\n disabled?: boolean;\n width?: CSSProperties[\"width\"];\n name?: string;\n form?: string;\n};\n\nconst Dropdown = ({\n options,\n placeholder = \"Select an option\",\n value,\n defaultValue = \"\",\n onChange,\n disabled,\n width = \"100%\",\n name,\n form,\n}: DropdownProps) => {\n const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);\n const [isOpen, setIsOpen] = useState(false);\n const dropdownRef = useRef<HTMLDivElement>(null);\n const hiddenInputRef = useRef<HTMLInputElement>(null);\n const selectedValue = value ?? uncontrolledValue;\n const selectedOptionIndex = options.findIndex(\n (option) => option.value === selectedValue,\n );\n const initialActiveOptionIndex = Math.max(0, selectedOptionIndex);\n const [activeOptionIndex, setActiveOptionIndex] = useState(\n initialActiveOptionIndex,\n );\n const triggerButtonRef = useRef<HTMLButtonElement>(null);\n\n useEffect(() => {\n if (!isOpen) return;\n\n const close = (event: PointerEvent) => {\n if (!dropdownRef.current?.contains(event.target as Node))\n setIsOpen(false);\n };\n\n document.addEventListener(\"pointerdown\", close);\n return () => document.removeEventListener(\"pointerdown\", close);\n }, [isOpen]);\n\n useEffect(() => {\n const formElement = hiddenInputRef.current?.form;\n if (!formElement || value !== undefined) return;\n\n const reset = () => {\n setUncontrolledValue(defaultValue);\n setIsOpen(false);\n };\n formElement.addEventListener(\"reset\", reset);\n return () => formElement.removeEventListener(\"reset\", reset);\n }, [defaultValue, value]);\n\n const openOptions = (index = initialActiveOptionIndex) => {\n if (!options.length) return;\n setActiveOptionIndex(index);\n setIsOpen(true);\n };\n\n const selectOption = (index: number) => {\n const option = options[index];\n if (!option) return;\n if (value === undefined) setUncontrolledValue(option.value);\n onChange?.(option.value);\n setIsOpen(false);\n triggerButtonRef.current?.focus();\n };\n\n return (\n <Container\n ref={dropdownRef}\n $width={width}\n onBlur={(event) => {\n if (!event.currentTarget.contains(event.relatedTarget))\n setIsOpen(false);\n }}\n >\n <Trigger\n ref={triggerButtonRef}\n $isOpen={isOpen}\n type=\"button\"\n disabled={disabled}\n onClick={() => (isOpen ? setIsOpen(false) : openOptions())}\n >\n <span>{options[selectedOptionIndex]?.label ?? placeholder}</span>\n <CaretDownIcon $isOpen={isOpen} />\n </Trigger>\n\n {name && (\n <input\n ref={hiddenInputRef}\n type=\"hidden\"\n name={name}\n form={form}\n value={selectedValue}\n disabled={disabled}\n />\n )}\n\n {isOpen && (\n <Options role=\"listbox\" tabIndex={-1} autoFocus>\n {options.map((option, index) => (\n <OptionItem\n key={option.value}\n role=\"option\"\n $isActive={index === activeOptionIndex}\n onClick={() => selectOption(index)}\n onMouseEnter={() => setActiveOptionIndex(index)}\n >\n {option.label}\n </OptionItem>\n ))}\n </Options>\n )}\n </Container>\n );\n};\n\nexport default Dropdown;\n\nconst Container = styled.div<{ $width: CSSProperties[\"width\"] }>`\n position: relative;\n width: ${({ $width }) =>\n typeof $width === \"number\" ? `${$width}px` : $width};\n`;\n\nconst Trigger = styled.button<{ $isOpen: boolean }>`\n box-sizing: border-box;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: ${Spacing.px12};\n width: 100%;\n padding: ${Spacing.px12} ${Spacing.px16};\n border: 1px solid ${Color.neutral.grey2};\n border-radius: ${Spacing.px8};\n\n ${applyTypography(Typography.bodyPrimaryMedium)}\n\n color: ${Color.neutral.black};\n background: ${({ $isOpen }) =>\n $isOpen ? Color.accent.blue3 : Color.neutral.white};\n cursor: pointer;\n\n > span {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n\n &:disabled {\n color: ${Color.typography.blackLow};\n background: ${Color.neutral.grey4};\n cursor: not-allowed;\n }\n`;\n\nconst CaretDownIcon = styled(SystemIcon.CaretDownIcon).attrs({ size: 20 })<{\n $isOpen: boolean;\n}>`\n flex: none;\n transform: rotate(${({ $isOpen }) => ($isOpen ? \"180deg\" : \"0\")});\n`;\n\nconst Options = styled.div`\n position: absolute;\n z-index: 1;\n top: calc(100% + ${Spacing.px8});\n left: 0;\n box-sizing: border-box;\n width: 100%;\n max-height: 320px;\n overflow-y: auto;\n padding: ${Spacing.px4} 0;\n border: 1px solid ${Color.neutral.grey3};\n border-radius: ${Spacing.px8};\n background: ${Color.neutral.white};\n`;\n\nconst OptionItem = styled.div<{ $isActive: boolean }>`\n padding: 10px ${Spacing.px16};\n\n ${applyTypography(Typography.bodyPrimaryRegular)}\n\n color: ${Color.neutral.black};\n background: ${({ $isActive }) =>\n $isActive ? Color.accent.yellow3 : Color.neutral.white};\n cursor: pointer;\n`;\n"]}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@grantbii/design-system",
3
3
  "author": "Grantbii",
4
4
  "license": "UNLICENSED",
5
- "version": "2.0.0",
5
+ "version": "3.0.3",
6
6
  "description": "Grantbii's Design System",
7
7
  "homepage": "https://design.grantbii.com",
8
8
  "repository": {
@@ -35,21 +35,21 @@
35
35
  "devDependencies": {
36
36
  "@chromatic-com/storybook": "5.2.1",
37
37
  "@grantbii/ui-core": "1.10.0",
38
- "@storybook/addon-a11y": "10.4.6",
39
- "@storybook/addon-docs": "10.4.6",
40
- "@storybook/addon-onboarding": "10.4.6",
41
- "@storybook/addon-vitest": "10.4.6",
42
- "@storybook/nextjs-vite": "10.4.6",
38
+ "@storybook/addon-a11y": "10.5.2",
39
+ "@storybook/addon-docs": "10.5.2",
40
+ "@storybook/addon-onboarding": "10.5.2",
41
+ "@storybook/addon-vitest": "10.5.2",
42
+ "@storybook/nextjs-vite": "10.5.2",
43
43
  "@types/node": "22.19.17",
44
44
  "@types/react": "19.2.17",
45
45
  "@types/react-dom": "19.2.3",
46
- "@vitest/browser": "4.1.9",
47
- "@vitest/browser-playwright": "4.1.9",
48
- "@vitest/coverage-v8": "4.1.9",
46
+ "@vitest/browser": "4.1.10",
47
+ "@vitest/browser-playwright": "4.1.10",
48
+ "@vitest/coverage-v8": "4.1.10",
49
49
  "eslint": "9.39.4",
50
50
  "eslint-config-next": "16.2.10",
51
51
  "eslint-config-prettier": "10.1.8",
52
- "eslint-plugin-storybook": "10.4.6",
52
+ "eslint-plugin-storybook": "10.5.2",
53
53
  "husky": "9.1.7",
54
54
  "lint-staged": "17.0.8",
55
55
  "next": "16.2.10",
@@ -57,10 +57,10 @@
57
57
  "prettier": "3.9.4",
58
58
  "react": "19.2.7",
59
59
  "react-dom": "19.2.7",
60
- "storybook": "10.4.6",
60
+ "storybook": "10.5.2",
61
61
  "styled-components": "6.4.3",
62
62
  "typescript": "6.0.3",
63
- "vitest": "4.1.9"
63
+ "vitest": "4.1.10"
64
64
  },
65
65
  "lint-staged": {
66
66
  "**/*.{html,css,json,md,yaml}": [
@@ -0,0 +1,9 @@
1
+ import { type ToastOptions } from "@/.";
2
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
3
+ declare const meta: Meta<ToastOptions>;
4
+ export default meta;
5
+ type Story = StoryObj<typeof meta>;
6
+ export declare const Neutral: Story;
7
+ export declare const WithAction: Story;
8
+ export declare const DescriptionOnly: Story;
9
+ export declare const Stacked: Story;
@@ -0,0 +1,106 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Button, Toast, ToastProvider, useToast } from "@/.";
3
+ import { useEffect } from "react";
4
+ import { fn } from "storybook/test";
5
+ import styled from "styled-components";
6
+ const meta = {
7
+ title: "Molecules/Toast",
8
+ component: Toast,
9
+ tags: ["autodocs"],
10
+ args: {
11
+ title: "Bookmarked Successfully",
12
+ message: "This grant has been added to your bookmark list & collections.",
13
+ variant: "neutral",
14
+ duration: 8000,
15
+ onClose: fn(),
16
+ },
17
+ argTypes: {
18
+ action: {
19
+ description: "Optional action displayed before the close button.",
20
+ control: false,
21
+ },
22
+ duration: {
23
+ description: "Time in milliseconds before the toast is dismissed.",
24
+ control: { type: "number", min: 0, step: 1000 },
25
+ table: { defaultValue: { summary: "8000" } },
26
+ },
27
+ dedupeKey: {
28
+ description: "Stable event key used to re-emphasize an existing toast instead of stacking a duplicate.",
29
+ control: "text",
30
+ },
31
+ },
32
+ parameters: {
33
+ docs: {
34
+ description: {
35
+ component: "Shows up to three notifications with the latest in front. Keep titles under approximately 40 characters, use one toast per event, and avoid placing toasts over navigation, modals, or form actions. On mobile, set `--toast-mobile-bottom-offset` when the navigation bar needs additional clearance.",
36
+ },
37
+ },
38
+ },
39
+ };
40
+ export default meta;
41
+ const ToastTriggerExample = (options) => {
42
+ const showToast = useToast();
43
+ return _jsx(Button, { label: "Show toast", onClick: () => showToast(options) });
44
+ };
45
+ const STACKED_TOAST_TITLES = [
46
+ "Report ready",
47
+ "Invite sent",
48
+ "Changes saved",
49
+ "File uploaded",
50
+ ];
51
+ const StackedToastPreview = (options) => {
52
+ const showToast = useToast();
53
+ useEffect(() => {
54
+ STACKED_TOAST_TITLES.forEach((title) => showToast({ ...options, title, dedupeKey: title }));
55
+ }, [options, showToast]);
56
+ return null;
57
+ };
58
+ const StackedToastTrigger = (options) => {
59
+ const showToast = useToast();
60
+ return (_jsx(Button, { label: "Show stacked toasts", onClick: () => STACKED_TOAST_TITLES.forEach((title) => showToast({ ...options, title, dedupeKey: title })) }));
61
+ };
62
+ const renderToast = (args, { viewMode }) => viewMode === "docs" ? (_jsx(Toast, { ...args })) : (_jsx(ToastProvider, { children: _jsx(ToastTriggerExample, { ...args }) }));
63
+ export const Neutral = {
64
+ render: renderToast,
65
+ };
66
+ export const WithAction = {
67
+ args: {
68
+ title: "Report ready",
69
+ message: "Your Q2 report has finished exporting",
70
+ variant: "success",
71
+ action: { label: "View", onClick: fn() },
72
+ },
73
+ render: renderToast,
74
+ };
75
+ export const DescriptionOnly = {
76
+ args: {
77
+ title: undefined,
78
+ message: "Copied to clipboard.",
79
+ duration: 4000,
80
+ },
81
+ render: renderToast,
82
+ };
83
+ export const Stacked = {
84
+ args: {
85
+ duration: 60000,
86
+ message: "photo.png was added to your library.",
87
+ variant: "success",
88
+ },
89
+ render: (args, { viewMode }) => viewMode === "docs" ? (_jsx(StackedPreview, { onClickCapture: (event) => {
90
+ event.preventDefault();
91
+ event.stopPropagation();
92
+ }, children: _jsx(ToastProvider, { children: _jsx(StackedToastPreview, { ...args }) }) })) : (_jsx(ToastProvider, { children: _jsx(StackedToastTrigger, { ...args }) })),
93
+ };
94
+ const StackedPreview = styled.div `
95
+ display: grid;
96
+ place-items: center;
97
+ width: 100%;
98
+ min-height: 112px;
99
+
100
+ [data-slot="toast-viewport"] {
101
+ position: relative;
102
+ inset: auto;
103
+ transform: none;
104
+ }
105
+ `;
106
+ //# sourceMappingURL=Toast.stories.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Toast.stories.js","sourceRoot":"","sources":["../../../stories/molecules/Toast.stories.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Button, Toast, ToastProvider, useToast, type ToastOptions } from \"@/.\";\nimport type { Meta, StoryObj } from \"@storybook/nextjs-vite\";\nimport { useEffect } from \"react\";\nimport { fn } from \"storybook/test\";\nimport styled from \"styled-components\";\n\nconst meta: Meta<ToastOptions> = {\n title: \"Molecules/Toast\",\n component: Toast,\n tags: [\"autodocs\"],\n args: {\n title: \"Bookmarked Successfully\",\n message: \"This grant has been added to your bookmark list & collections.\",\n variant: \"neutral\",\n duration: 8000,\n onClose: fn(),\n },\n argTypes: {\n action: {\n description: \"Optional action displayed before the close button.\",\n control: false,\n },\n duration: {\n description: \"Time in milliseconds before the toast is dismissed.\",\n control: { type: \"number\", min: 0, step: 1000 },\n table: { defaultValue: { summary: \"8000\" } },\n },\n dedupeKey: {\n description:\n \"Stable event key used to re-emphasize an existing toast instead of stacking a duplicate.\",\n control: \"text\",\n },\n },\n parameters: {\n docs: {\n description: {\n component:\n \"Shows up to three notifications with the latest in front. Keep titles under approximately 40 characters, use one toast per event, and avoid placing toasts over navigation, modals, or form actions. On mobile, set `--toast-mobile-bottom-offset` when the navigation bar needs additional clearance.\",\n },\n },\n },\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof meta>;\n\nconst ToastTriggerExample = (options: ToastOptions) => {\n const showToast = useToast();\n return <Button label=\"Show toast\" onClick={() => showToast(options)} />;\n};\n\nconst STACKED_TOAST_TITLES = [\n \"Report ready\",\n \"Invite sent\",\n \"Changes saved\",\n \"File uploaded\",\n];\n\nconst StackedToastPreview = (options: ToastOptions) => {\n const showToast = useToast();\n\n useEffect(() => {\n STACKED_TOAST_TITLES.forEach((title) =>\n showToast({ ...options, title, dedupeKey: title }),\n );\n }, [options, showToast]);\n\n return null;\n};\n\nconst StackedToastTrigger = (options: ToastOptions) => {\n const showToast = useToast();\n\n return (\n <Button\n label=\"Show stacked toasts\"\n onClick={() =>\n STACKED_TOAST_TITLES.forEach((title) =>\n showToast({ ...options, title, dedupeKey: title }),\n )\n }\n />\n );\n};\n\nconst renderToast: NonNullable<Story[\"render\"]> = (args, { viewMode }) =>\n viewMode === \"docs\" ? (\n <Toast {...args} />\n ) : (\n <ToastProvider>\n <ToastTriggerExample {...args} />\n </ToastProvider>\n );\n\nexport const Neutral: Story = {\n render: renderToast,\n};\n\nexport const WithAction: Story = {\n args: {\n title: \"Report ready\",\n message: \"Your Q2 report has finished exporting\",\n variant: \"success\",\n action: { label: \"View\", onClick: fn() },\n },\n render: renderToast,\n};\n\nexport const DescriptionOnly: Story = {\n args: {\n title: undefined,\n message: \"Copied to clipboard.\",\n duration: 4000,\n },\n render: renderToast,\n};\n\nexport const Stacked: Story = {\n args: {\n duration: 60000,\n message: \"photo.png was added to your library.\",\n variant: \"success\",\n },\n render: (args, { viewMode }) =>\n viewMode === \"docs\" ? (\n <StackedPreview\n onClickCapture={(event) => {\n event.preventDefault();\n event.stopPropagation();\n }}\n >\n <ToastProvider>\n <StackedToastPreview {...args} />\n </ToastProvider>\n </StackedPreview>\n ) : (\n <ToastProvider>\n <StackedToastTrigger {...args} />\n </ToastProvider>\n ),\n};\n\nconst StackedPreview = styled.div`\n display: grid;\n place-items: center;\n width: 100%;\n min-height: 112px;\n\n [data-slot=\"toast-viewport\"] {\n position: relative;\n inset: auto;\n transform: none;\n }\n`;\n"]}
@@ -0,0 +1,11 @@
1
+ import { type TooltipContentProps } from "@/.";
2
+ import type { ReactNode } from "react";
3
+ import type { Meta, StoryObj } from "@storybook/nextjs-vite";
4
+ type TooltipExampleProps = Omit<TooltipContentProps, "children"> & {
5
+ content: ReactNode;
6
+ };
7
+ declare const TooltipExample: ({ content, ...tooltipContentProps }: TooltipExampleProps) => import("react").JSX.Element;
8
+ declare const meta: Meta<typeof TooltipExample>;
9
+ export default meta;
10
+ type Story = StoryObj<typeof meta>;
11
+ export declare const Default: Story;
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button, Tooltip, TooltipContent, TooltipTrigger, } from "@/.";
3
+ const TooltipExample = ({ content, ...tooltipContentProps }) => (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { children: _jsx(Button, { label: "Hover me" }) }), _jsx(TooltipContent, { ...tooltipContentProps, children: content })] }));
4
+ const meta = {
5
+ title: "Molecules/Tooltip",
6
+ component: TooltipExample,
7
+ tags: ["autodocs"],
8
+ args: {
9
+ content: "A message which appears when a cursor is positioned over an element.",
10
+ side: "top",
11
+ sideOffset: 8,
12
+ },
13
+ argTypes: {
14
+ content: {
15
+ description: "Content displayed inside the tooltip.",
16
+ control: "text",
17
+ },
18
+ side: {
19
+ description: "Preferred side of the trigger where the tooltip is displayed. It may change to remain visible within the viewport.",
20
+ control: "radio",
21
+ options: ["top", "right", "bottom", "left"],
22
+ table: { defaultValue: { summary: '"top"' } },
23
+ },
24
+ sideOffset: {
25
+ description: "Distance in pixels between the tooltip and its trigger.",
26
+ control: { type: "number", min: 0, step: 1 },
27
+ table: { defaultValue: { summary: "0" } },
28
+ },
29
+ },
30
+ };
31
+ export default meta;
32
+ export const Default = {};
33
+ //# sourceMappingURL=Tooltip.stories.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Tooltip.stories.js","sourceRoot":"","sources":["../../../stories/molecules/Tooltip.stories.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,MAAM,EACN,OAAO,EACP,cAAc,EACd,cAAc,GAEf,MAAM,KAAK,CAAC;AAQb,MAAM,cAAc,GAAG,CAAC,EACtB,OAAO,EACP,GAAG,mBAAmB,EACF,EAAE,EAAE,CAAC,CACzB,MAAC,OAAO,eACN,KAAC,cAAc,cACb,KAAC,MAAM,IAAC,KAAK,EAAC,UAAU,GAAG,GACZ,EACjB,KAAC,cAAc,OAAK,mBAAmB,YAAG,OAAO,GAAkB,IAC3D,CACX,CAAC;AAEF,MAAM,IAAI,GAAgC;IACxC,KAAK,EAAE,mBAAmB;IAC1B,SAAS,EAAE,cAAc;IACzB,IAAI,EAAE,CAAC,UAAU,CAAC;IAClB,IAAI,EAAE;QACJ,OAAO,EACL,sEAAsE;QACxE,IAAI,EAAE,KAAK;QACX,UAAU,EAAE,CAAC;KACd;IACD,QAAQ,EAAE;QACR,OAAO,EAAE;YACP,WAAW,EAAE,uCAAuC;YACpD,OAAO,EAAE,MAAM;SAChB;QACD,IAAI,EAAE;YACJ,WAAW,EACT,oHAAoH;YACtH,OAAO,EAAE,OAAO;YAChB,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC;YAC3C,KAAK,EAAE,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE;SAC9C;QACD,UAAU,EAAE;YACV,WAAW,EAAE,yDAAyD;YACtE,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE;YAC5C,KAAK,EAAE,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,EAAE;SAC1C;KACF;CACF,CAAC;AAEF,eAAe,IAAI,CAAC;AAIpB,MAAM,CAAC,MAAM,OAAO,GAAU,EAAE,CAAC","sourcesContent":["import {\n Button,\n Tooltip,\n TooltipContent,\n TooltipTrigger,\n type TooltipContentProps,\n} from \"@/.\";\nimport type { ReactNode } from \"react\";\nimport type { Meta, StoryObj } from \"@storybook/nextjs-vite\";\n\ntype TooltipExampleProps = Omit<TooltipContentProps, \"children\"> & {\n content: ReactNode;\n};\n\nconst TooltipExample = ({\n content,\n ...tooltipContentProps\n}: TooltipExampleProps) => (\n <Tooltip>\n <TooltipTrigger>\n <Button label=\"Hover me\" />\n </TooltipTrigger>\n <TooltipContent {...tooltipContentProps}>{content}</TooltipContent>\n </Tooltip>\n);\n\nconst meta: Meta<typeof TooltipExample> = {\n title: \"Molecules/Tooltip\",\n component: TooltipExample,\n tags: [\"autodocs\"],\n args: {\n content:\n \"A message which appears when a cursor is positioned over an element.\",\n side: \"top\",\n sideOffset: 8,\n },\n argTypes: {\n content: {\n description: \"Content displayed inside the tooltip.\",\n control: \"text\",\n },\n side: {\n description:\n \"Preferred side of the trigger where the tooltip is displayed. It may change to remain visible within the viewport.\",\n control: \"radio\",\n options: [\"top\", \"right\", \"bottom\", \"left\"],\n table: { defaultValue: { summary: '\"top\"' } },\n },\n sideOffset: {\n description: \"Distance in pixels between the tooltip and its trigger.\",\n control: { type: \"number\", min: 0, step: 1 },\n table: { defaultValue: { summary: \"0\" } },\n },\n },\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof meta>;\n\nexport const Default: Story = {};\n"]}
@@ -1,10 +1,12 @@
1
+ import { type DropdownProps } from "@/.";
1
2
  import type { Meta, StoryObj } from "@storybook/nextjs-vite";
2
- type ExampleProps = {
3
+ type ExampleProps = DropdownProps & {
3
4
  controlled: boolean;
4
5
  };
5
- declare const DropdownExample: ({ controlled }: ExampleProps) => import("react").JSX.Element;
6
+ declare const DropdownExample: ({ controlled, onChange, ...props }: ExampleProps) => import("react").JSX.Element;
6
7
  declare const meta: Meta<typeof DropdownExample>;
7
8
  export default meta;
8
9
  type Story = StoryObj<typeof meta>;
9
10
  export declare const Controlled: Story;
10
11
  export declare const Uncontrolled: Story;
12
+ export declare const FitContent: Story;
@@ -1,39 +1,96 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Dropdown, enumToOptions } from "@/.";
3
3
  import { Objective } from "@grantbii/ui-core/grant/enums";
4
- import { parseEnum } from "@grantbii/ui-core/enums";
5
4
  import { useState } from "react";
6
- const DropdownExample = ({ controlled }) => {
7
- const [selectedObjective, setSelectedObjective] = useState(Objective.UNKNOWN);
8
- const controlledProps = {
9
- ...baseProps,
10
- value: selectedObjective === Objective.UNKNOWN ? "" : selectedObjective,
11
- onChange: (event) => setSelectedObjective(parseEnum(event.target.value, Objective)),
5
+ import { expect, fn, userEvent, within } from "storybook/test";
6
+ const DropdownExample = ({ controlled, onChange, ...props }) => {
7
+ const [value, setValue] = useState(props.defaultValue ?? "");
8
+ const handleChange = (nextValue) => {
9
+ setValue(nextValue);
10
+ onChange?.(nextValue);
12
11
  };
13
- return _jsx(Dropdown, { ...(controlled ? controlledProps : uncontrolledProps) });
14
- };
15
- const baseProps = {
16
- options: enumToOptions(Objective),
17
- defaultLabel: "Select grant objective",
18
- };
19
- const uncontrolledProps = {
20
- ...baseProps,
21
- defaultValue: "",
12
+ return (_jsx(Dropdown, { ...props, ...(controlled
13
+ ? { value: props.value ?? value, onChange: handleChange }
14
+ : { onChange }) }));
22
15
  };
23
16
  const meta = {
24
17
  title: "Organisms/Dropdown",
25
18
  component: DropdownExample,
26
19
  tags: ["autodocs"],
20
+ args: {
21
+ controlled: false,
22
+ options: enumToOptions(Objective).slice(0, 5),
23
+ placeholder: "Select grant objective",
24
+ defaultValue: "",
25
+ disabled: false,
26
+ width: 320,
27
+ name: "objective",
28
+ onChange: fn(),
29
+ },
30
+ argTypes: {
31
+ controlled: {
32
+ description: "Controls whether the selected value is managed by React state or by the Dropdown itself.",
33
+ table: { defaultValue: { summary: "false" } },
34
+ },
35
+ options: {
36
+ description: "Options displayed in the dropdown panel.",
37
+ control: "object",
38
+ },
39
+ placeholder: {
40
+ description: "Label displayed when no option is selected.",
41
+ table: { defaultValue: { summary: '"Select an option"' } },
42
+ control: "text",
43
+ },
44
+ value: {
45
+ description: "Selected value when the Dropdown is controlled.",
46
+ control: "text",
47
+ },
48
+ defaultValue: {
49
+ description: "Initial selected value when the Dropdown is uncontrolled.",
50
+ table: { defaultValue: { summary: '""' } },
51
+ control: "text",
52
+ },
53
+ onChange: {
54
+ description: "Called with the selected option value.",
55
+ control: false,
56
+ },
57
+ disabled: {
58
+ description: "Disables the dropdown trigger and form value.",
59
+ table: { defaultValue: { summary: "false" } },
60
+ control: "boolean",
61
+ },
62
+ width: {
63
+ description: "Width of the trigger and dropdown container.",
64
+ table: { defaultValue: { summary: '"100%"' } },
65
+ control: "text",
66
+ },
67
+ name: {
68
+ description: "Form field name used to submit the selected value.",
69
+ control: "text",
70
+ },
71
+ form: {
72
+ description: "ID of the form associated with the hidden form field.",
73
+ control: "text",
74
+ },
75
+ },
27
76
  };
28
77
  export default meta;
29
78
  export const Controlled = {
30
79
  args: {
31
80
  controlled: true,
32
81
  },
82
+ play: async ({ canvasElement }) => {
83
+ const canvas = within(canvasElement);
84
+ const trigger = canvas.getByRole("button");
85
+ await userEvent.click(trigger);
86
+ await userEvent.click(canvas.getAllByRole("option")[0]);
87
+ await expect(trigger).not.toHaveTextContent("Select grant objective");
88
+ },
33
89
  };
34
- export const Uncontrolled = {
90
+ export const Uncontrolled = {};
91
+ export const FitContent = {
35
92
  args: {
36
- controlled: false,
93
+ width: "fit-content",
37
94
  },
38
95
  };
39
96
  //# sourceMappingURL=Dropdown.stories.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"Dropdown.stories.js","sourceRoot":"","sources":["../../../stories/organisms/Dropdown.stories.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAsB,MAAM,KAAK,CAAC;AAClE,OAAO,EAAE,SAAS,EAAE,MAAM,+BAA+B,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEpD,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAMjC,MAAM,eAAe,GAAG,CAAC,EAAE,UAAU,EAAgB,EAAE,EAAE;IACvD,MAAM,CAAC,iBAAiB,EAAE,oBAAoB,CAAC,GAAG,QAAQ,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAE9E,MAAM,eAAe,GAAkB;QACrC,GAAG,SAAS;QACZ,KAAK,EAAE,iBAAiB,KAAK,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,iBAAiB;QACvE,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAClB,oBAAoB,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;KACjE,CAAC;IAEF,OAAO,KAAC,QAAQ,OAAK,CAAC,UAAU,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,iBAAiB,CAAC,GAAI,CAAC;AAC9E,CAAC,CAAC;AAEF,MAAM,SAAS,GAAkB;IAC/B,OAAO,EAAE,aAAa,CAAC,SAAS,CAAC;IACjC,YAAY,EAAE,wBAAwB;CACvC,CAAC;AAEF,MAAM,iBAAiB,GAAkB;IACvC,GAAG,SAAS;IACZ,YAAY,EAAE,EAAE;CACjB,CAAC;AAEF,MAAM,IAAI,GAAiC;IACzC,KAAK,EAAE,oBAAoB;IAC3B,SAAS,EAAE,eAAe;IAC1B,IAAI,EAAE,CAAC,UAAU,CAAC;CACnB,CAAC;AAEF,eAAe,IAAI,CAAC;AAIpB,MAAM,CAAC,MAAM,UAAU,GAAU;IAC/B,IAAI,EAAE;QACJ,UAAU,EAAE,IAAI;KACjB;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAU;IACjC,IAAI,EAAE;QACJ,UAAU,EAAE,KAAK;KAClB;CACF,CAAC","sourcesContent":["import { Dropdown, enumToOptions, type DropdownProps } from \"@/.\";\nimport { Objective } from \"@grantbii/ui-core/grant/enums\";\nimport { parseEnum } from \"@grantbii/ui-core/enums\";\nimport type { Meta, StoryObj } from \"@storybook/nextjs-vite\";\nimport { useState } from \"react\";\n\ntype ExampleProps = {\n controlled: boolean;\n};\n\nconst DropdownExample = ({ controlled }: ExampleProps) => {\n const [selectedObjective, setSelectedObjective] = useState(Objective.UNKNOWN);\n\n const controlledProps: DropdownProps = {\n ...baseProps,\n value: selectedObjective === Objective.UNKNOWN ? \"\" : selectedObjective,\n onChange: (event) =>\n setSelectedObjective(parseEnum(event.target.value, Objective)),\n };\n\n return <Dropdown {...(controlled ? controlledProps : uncontrolledProps)} />;\n};\n\nconst baseProps: DropdownProps = {\n options: enumToOptions(Objective),\n defaultLabel: \"Select grant objective\",\n};\n\nconst uncontrolledProps: DropdownProps = {\n ...baseProps,\n defaultValue: \"\",\n};\n\nconst meta: Meta<typeof DropdownExample> = {\n title: \"Organisms/Dropdown\",\n component: DropdownExample,\n tags: [\"autodocs\"],\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof meta>;\n\nexport const Controlled: Story = {\n args: {\n controlled: true,\n },\n};\n\nexport const Uncontrolled: Story = {\n args: {\n controlled: false,\n },\n};\n"]}
1
+ {"version":3,"file":"Dropdown.stories.js","sourceRoot":"","sources":["../../../stories/organisms/Dropdown.stories.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Dropdown, enumToOptions, type DropdownProps } from \"@/.\";\nimport { Objective } from \"@grantbii/ui-core/grant/enums\";\nimport type { Meta, StoryObj } from \"@storybook/nextjs-vite\";\nimport { useState } from \"react\";\nimport { expect, fn, userEvent, within } from \"storybook/test\";\n\ntype ExampleProps = DropdownProps & {\n controlled: boolean;\n};\n\nconst DropdownExample = ({ controlled, onChange, ...props }: ExampleProps) => {\n const [value, setValue] = useState(props.defaultValue ?? \"\");\n\n const handleChange = (nextValue: string) => {\n setValue(nextValue);\n onChange?.(nextValue);\n };\n\n return (\n <Dropdown\n {...props}\n {...(controlled\n ? { value: props.value ?? value, onChange: handleChange }\n : { onChange })}\n />\n );\n};\n\nconst meta: Meta<typeof DropdownExample> = {\n title: \"Organisms/Dropdown\",\n component: DropdownExample,\n tags: [\"autodocs\"],\n args: {\n controlled: false,\n options: enumToOptions(Objective).slice(0, 5),\n placeholder: \"Select grant objective\",\n defaultValue: \"\",\n disabled: false,\n width: 320,\n name: \"objective\",\n onChange: fn(),\n },\n argTypes: {\n controlled: {\n description:\n \"Controls whether the selected value is managed by React state or by the Dropdown itself.\",\n table: { defaultValue: { summary: \"false\" } },\n },\n options: {\n description: \"Options displayed in the dropdown panel.\",\n control: \"object\",\n },\n placeholder: {\n description: \"Label displayed when no option is selected.\",\n table: { defaultValue: { summary: '\"Select an option\"' } },\n control: \"text\",\n },\n value: {\n description: \"Selected value when the Dropdown is controlled.\",\n control: \"text\",\n },\n defaultValue: {\n description: \"Initial selected value when the Dropdown is uncontrolled.\",\n table: { defaultValue: { summary: '\"\"' } },\n control: \"text\",\n },\n onChange: {\n description: \"Called with the selected option value.\",\n control: false,\n },\n disabled: {\n description: \"Disables the dropdown trigger and form value.\",\n table: { defaultValue: { summary: \"false\" } },\n control: \"boolean\",\n },\n width: {\n description: \"Width of the trigger and dropdown container.\",\n table: { defaultValue: { summary: '\"100%\"' } },\n control: \"text\",\n },\n name: {\n description: \"Form field name used to submit the selected value.\",\n control: \"text\",\n },\n form: {\n description: \"ID of the form associated with the hidden form field.\",\n control: \"text\",\n },\n },\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof meta>;\n\nexport const Controlled: Story = {\n args: {\n controlled: true,\n },\n play: async ({ canvasElement }) => {\n const canvas = within(canvasElement);\n const trigger = canvas.getByRole(\"button\");\n await userEvent.click(trigger);\n await userEvent.click(canvas.getAllByRole(\"option\")[0]);\n await expect(trigger).not.toHaveTextContent(\"Select grant objective\");\n },\n};\n\nexport const Uncontrolled: Story = {};\n\nexport const FitContent: Story = {\n args: {\n width: \"fit-content\",\n },\n};\n"]}