From fd1783d85428edd55294e70547d9d9187e30879c Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 2 Oct 2026 14:56:31 +0900 Subject: [PATCH 1/3] fix(react,components): support React 18 peers and drop React 19-only APIs Refs #691 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../changepack_log_react_18_support.json | 8 ++ bun.lock | 30 +++--- packages/components/package.json | 6 +- .../Select/__tests__/index.browser.test.tsx | 3 +- .../src/components/Select/index.tsx | 93 ++++++++++--------- .../src/components/Stepper/index.tsx | 4 +- packages/components/src/contexts/useSelect.ts | 4 +- packages/react/package.json | 6 +- 8 files changed, 84 insertions(+), 70 deletions(-) create mode 100644 .changepacks/changepack_log_react_18_support.json diff --git a/.changepacks/changepack_log_react_18_support.json b/.changepacks/changepack_log_react_18_support.json new file mode 100644 index 000000000..8fbde2ca0 --- /dev/null +++ b/.changepacks/changepack_log_react_18_support.json @@ -0,0 +1,8 @@ +{ + "changes": { + "packages/react/package.json": "Patch", + "packages/components/package.json": "Patch" + }, + "note": "@devup-ui/react and @devup-ui/components take React as a peer of ^18 || ^19 instead of `*` and no longer depend on React 19 themselves, so a React 18 app no longer gets a second React. Select and Stepper read their context with useContext instead of React 19's use(), and Select repositions its container from an effect instead of a callback ref returning cleanup, which React 18 ignores", + "date": "2026-10-01T00:00:00.000Z" +} diff --git a/bun.lock b/bun.lock index 9eea57e19..36af23d4d 100644 --- a/bun.lock +++ b/bun.lock @@ -469,11 +469,11 @@ }, "bindings/devup-ui-wasm": { "name": "@devup-ui/wasm", - "version": "1.0.80", + "version": "1.0.82", }, "packages/bun-plugin": { "name": "@devup-ui/bun-plugin", - "version": "1.0.19", + "version": "1.0.21", "dependencies": { "@devup-ui/plugin-utils": "workspace:^", "@devup-ui/wasm": "workspace:^", @@ -488,11 +488,10 @@ }, "packages/components": { "name": "@devup-ui/components", - "version": "0.1.57", + "version": "0.1.59", "dependencies": { "@devup-ui/react": "workspace:^", "clsx": "^2.1", - "react": "^19.3.0", }, "devDependencies": { "@devup-ui/vite-plugin": "workspace:^", @@ -502,6 +501,7 @@ "@types/react": "^19.3", "@typescript/typescript6": "^6.0", "eslint-plugin-storybook": "^10.6", + "react": "^19.3.0", "rollup-plugin-preserve-directives": "^0.4", "storybook": "^10.6", "typescript": "^7.0.2", @@ -510,12 +510,12 @@ }, "peerDependencies": { "@devup-ui/react": "workspace:^", - "react": "*", + "react": "^18 || ^19", }, }, "packages/eslint-plugin": { "name": "@devup-ui/eslint-plugin", - "version": "1.0.20", + "version": "1.0.21", "dependencies": { "@typescript-eslint/utils": "^8.70", "typescript-eslint": "^8.70", @@ -532,7 +532,7 @@ }, "packages/next-plugin": { "name": "@devup-ui/next-plugin", - "version": "1.0.87", + "version": "1.0.89", "dependencies": { "@devup-ui/plugin-utils": "workspace:^", "@devup-ui/wasm": "workspace:^", @@ -550,22 +550,22 @@ }, "packages/plugin-utils": { "name": "@devup-ui/plugin-utils", - "version": "1.0.15", + "version": "1.0.16", "devDependencies": { "typescript": "^7.0.2", }, }, "packages/react": { "name": "@devup-ui/react", - "version": "1.0.43", + "version": "1.0.44", "dependencies": { "csstype-extra": "latest", - "react": "^19.3", }, "devDependencies": { "@types/node": "^26.6", "@types/react": "^19.3", "@typescript/typescript6": "^6.0", + "react": "^19.3", "rollup-plugin-preserve-directives": "^0.4", "typescript": "^7.0.2", "vite": "^8.3", @@ -573,12 +573,12 @@ }, "peerDependencies": { "csstype-extra": "*", - "react": "*", + "react": "^18 || ^19", }, }, "packages/reset-css": { "name": "@devup-ui/reset-css", - "version": "1.0.30", + "version": "1.0.31", "dependencies": { "@devup-ui/react": "workspace:^", }, @@ -591,7 +591,7 @@ }, "packages/rsbuild-plugin": { "name": "@devup-ui/rsbuild-plugin", - "version": "1.0.64", + "version": "1.0.66", "dependencies": { "@devup-ui/plugin-utils": "workspace:^", "@devup-ui/wasm": "workspace:^", @@ -607,7 +607,7 @@ }, "packages/vite-plugin": { "name": "@devup-ui/vite-plugin", - "version": "1.0.70", + "version": "1.0.72", "dependencies": { "@devup-ui/plugin-utils": "workspace:^", "@devup-ui/wasm": "workspace:^", @@ -622,7 +622,7 @@ }, "packages/webpack-plugin": { "name": "@devup-ui/webpack-plugin", - "version": "1.0.68", + "version": "1.0.70", "dependencies": { "@devup-ui/plugin-utils": "workspace:^", "@devup-ui/wasm": "workspace:^", diff --git a/packages/components/package.json b/packages/components/package.json index 25e552f51..212a785d8 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -44,7 +44,6 @@ "types": "./dist/index.d.ts", "dependencies": { "@devup-ui/react": "workspace:^", - "react": "^19.3.0", "clsx": "^2.1" }, "devDependencies": { @@ -59,10 +58,11 @@ "typescript": "^7.0.2", "@typescript/typescript6": "^6.0", "vite": "^8.3", - "vite-plugin-dts": "^5.1" + "vite-plugin-dts": "^5.1", + "react": "^19.3.0" }, "peerDependencies": { "@devup-ui/react": "workspace:^", - "react": "*" + "react": "^18 || ^19" } } diff --git a/packages/components/src/components/Select/__tests__/index.browser.test.tsx b/packages/components/src/components/Select/__tests__/index.browser.test.tsx index 116572383..444e4db22 100644 --- a/packages/components/src/components/Select/__tests__/index.browser.test.tsx +++ b/packages/components/src/components/Select/__tests__/index.browser.test.tsx @@ -449,7 +449,8 @@ describe('Select', () => { // offsetWidth > 1024px Object.defineProperty(selectContainer, 'offsetWidth', { value: 1100 }) - // rerender + // the container repositions when the window resizes + window.dispatchEvent(new Event('resize')) rerender() expect(container).toMatchSnapshot() diff --git a/packages/components/src/components/Select/index.tsx b/packages/components/src/components/Select/index.tsx index 4ce1b0220..7392925d5 100644 --- a/packages/components/src/components/Select/index.tsx +++ b/packages/components/src/components/Select/index.tsx @@ -230,54 +230,59 @@ export function SelectContainer({ ...props }: SelectContainerProps) { const { open, setOpen, type, ref } = useSelect() + const containerRef = useRef(null) - if (!open) return null - return ( - { - if (!ref.current || !el) return - const combobox = ref.current - - // 요소가 움직일 때마다(스크롤, 리사이즈 등) 위치를 갱신하도록 이벤트를 등록합니다. - const updatePosition = () => { - const { - height, - x: comboboxX, - y: comboboxY, - top, - left, - } = combobox.getBoundingClientRect() - - const isOverflowBottom = - el.offsetHeight + top + window.scrollY + height + y > - document.documentElement.scrollHeight - - const isOverflowRight = - el.offsetWidth + left + window.scrollX + x > - document.documentElement.scrollWidth - - if (isOverflowBottom) - el.style.bottom = `${window.innerHeight - comboboxY + 10}px` - else el.style.top = `${comboboxY + height + 10 + y}px` - - if (isOverflowRight) - el.style.left = `${Math.max(comboboxX - el.offsetWidth + combobox.offsetWidth, 0) + x}px` - else el.style.left = `${comboboxX + x}px` - } + // An effect rather than a callback ref returning cleanup, which React 18 ignores + useEffect(() => { + const el = containerRef.current + if (!open || !ref.current || !el) return + const combobox = ref.current - // 최초 위치 설정 - updatePosition() + // 요소가 움직일 때마다(스크롤, 리사이즈 등) 위치를 갱신하도록 이벤트를 등록합니다. + const updatePosition = () => { + const { + height, + x: comboboxX, + y: comboboxY, + top, + left, + } = combobox.getBoundingClientRect() - // 스크롤, 리사이즈, DOM 변경 등 요소 위치가 변할 수 있는 이벤트에 리스너 등록 - window.addEventListener('scroll', updatePosition, true) - window.addEventListener('resize', updatePosition) + const isOverflowBottom = + el.offsetHeight + top + window.scrollY + height + y > + document.documentElement.scrollHeight - // 컴포넌트 언마운트 시 이벤트 해제 - return () => { - window.removeEventListener('scroll', updatePosition, true) - window.removeEventListener('resize', updatePosition) - } - }} + const isOverflowRight = + el.offsetWidth + left + window.scrollX + x > + document.documentElement.scrollWidth + + if (isOverflowBottom) + el.style.bottom = `${window.innerHeight - comboboxY + 10}px` + else el.style.top = `${comboboxY + height + 10 + y}px` + + if (isOverflowRight) + el.style.left = `${Math.max(comboboxX - el.offsetWidth + combobox.offsetWidth, 0) + x}px` + else el.style.left = `${comboboxX + x}px` + } + + // 최초 위치 설정 + updatePosition() + + // 스크롤, 리사이즈, DOM 변경 등 요소 위치가 변할 수 있는 이벤트에 리스너 등록 + window.addEventListener('scroll', updatePosition, true) + window.addEventListener('resize', updatePosition) + + // 컴포넌트 언마운트 시 이벤트 해제 + return () => { + window.removeEventListener('scroll', updatePosition, true) + window.removeEventListener('resize', updatePosition) + } + }, [open, ref, x, y]) + + if (!open) return null + return ( + (null) export const useStepper = () => { - const context = use(StepperContext) + const context = useContext(StepperContext) if (!context) { throw new Error('useStepper must be used within a StepperProvider') } diff --git a/packages/components/src/contexts/useSelect.ts b/packages/components/src/contexts/useSelect.ts index cb716bae2..da05b8aa4 100644 --- a/packages/components/src/contexts/useSelect.ts +++ b/packages/components/src/contexts/useSelect.ts @@ -1,5 +1,5 @@ 'use client' -import { createContext, use } from 'react' +import { createContext, useContext } from 'react' type SelectType = 'default' | 'radio' | 'checkbox' type SelectValue = T extends 'radio' ? string : string[] @@ -14,7 +14,7 @@ export const SelectContext = createContext<{ } | null>(null) export const useSelect = () => { - const context = use(SelectContext) + const context = useContext(SelectContext) if (!context) { throw new Error('useSelect must be used within a Select') } diff --git a/packages/react/package.json b/packages/react/package.json index a05b1c916..9a37d2af0 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -66,7 +66,6 @@ ], "types": "./dist/index.d.ts", "dependencies": { - "react": "^19.3", "csstype-extra": "latest" }, "devDependencies": { @@ -76,10 +75,11 @@ "typescript": "^7.0.2", "@typescript/typescript6": "^6.0", "@types/react": "^19.3", - "@types/node": "^26.6" + "@types/node": "^26.6", + "react": "^19.3" }, "peerDependencies": { - "react": "*", + "react": "^18 || ^19", "csstype-extra": "*" } } From a641f0b80823a5c509f8b764a241bb834b598508 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Fri, 2 Oct 2026 15:13:18 +0900 Subject: [PATCH 2/3] chore: satisfy clippy assert_is_empty lint on Rust 1.99 Refs #691 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- bindings/devup-ui-wasm/src/lib.rs | 8 ++++---- libs/css/src/theme_tokens.rs | 2 +- libs/extractor/src/lib.rs | 8 ++++---- libs/extractor/src/tailwind.rs | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/bindings/devup-ui-wasm/src/lib.rs b/bindings/devup-ui-wasm/src/lib.rs index 4560410f3..0a2347ac2 100644 --- a/bindings/devup-ui-wasm/src/lib.rs +++ b/bindings/devup-ui-wasm/src/lib.rs @@ -1551,7 +1551,7 @@ mod tests { ); // Test getters - assert!(!output.code().is_empty()); + assert_ne!(output.code(), ""); assert_eq!(output.css_file(), Some("devup-ui-0.css".to_string())); assert_eq!(output.map(), Some("//# sourceMappingURL=test".to_string())); assert!(output.css().is_some()); @@ -1951,7 +1951,7 @@ mod tests { assert!(result.is_ok()); let output = result.unwrap(); - assert!(!output.code().is_empty()); + assert_ne!(output.code(), ""); assert!(output.map().is_some()); } @@ -1975,7 +1975,7 @@ mod tests { assert!(result.is_ok()); let output = result.unwrap(); - assert!(!output.code().is_empty()); + assert_ne!(output.code(), ""); assert!(output.map().is_none()); } @@ -2000,7 +2000,7 @@ mod tests { assert!(result.is_err()); if let Err(error) = result { - assert!(!error.is_empty()); + assert_ne!(error, ""); } } diff --git a/libs/css/src/theme_tokens.rs b/libs/css/src/theme_tokens.rs index d8d8fb021..3b2346aed 100644 --- a/libs/css/src/theme_tokens.rs +++ b/libs/css/src/theme_tokens.rs @@ -119,7 +119,7 @@ mod tests { set_typography_keys(vec!["body".to_string(), "title".to_string()]); assert_eq!(get_typography_keys(), vec!["body", "title"]); set_typography_keys(vec![]); - assert!(get_typography_keys().is_empty()); + assert_eq!(get_typography_keys(), Vec::::new()); } #[test] diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs index 68981f36c..96c6ca430 100644 --- a/libs/extractor/src/lib.rs +++ b/libs/extractor/src/lib.rs @@ -836,8 +836,8 @@ mod tests { alternate: None, }; - assert!(empty.extract().is_empty()); - assert!(empty.into_extract().is_empty()); + assert_eq!(empty.extract(), vec![]); + assert_eq!(empty.into_extract(), vec![]); } #[test] @@ -13598,7 +13598,7 @@ globalCss({ ); assert!(result.is_ok()); let output = result.unwrap(); - assert!(!output.code.is_empty()); + assert_ne!(output.code, ""); } #[test] @@ -18778,7 +18778,7 @@ export const k = styled('div')({ color: SIZE });", &memory_resolver(CONSTANT_MODULES), ) .unwrap(); - assert!(without_imports.dependencies.is_empty()); + assert_eq!(without_imports.dependencies.len(), 0); let without_constants = extract_with_modules( "/src/Handler.tsx", "import { Box } from '@devup-ui/react';\nimport { handler } from './handler';\nexport const a = ;", diff --git a/libs/extractor/src/tailwind.rs b/libs/extractor/src/tailwind.rs index 87e9f5178..70643aa7c 100644 --- a/libs/extractor/src/tailwind.rs +++ b/libs/extractor/src/tailwind.rs @@ -284,7 +284,7 @@ pub struct TailwindClass { /// non-overlapping) but mutates the existing buffer instead of allocating a new /// `String`. `needle` must be non-empty. fn remove_all_substr(haystack: &mut String, needle: &str) { - debug_assert!(!needle.is_empty()); + debug_assert_ne!(needle, ""); let mut search_from = 0; while let Some(rel) = haystack[search_from..].find(needle) { let at = search_from + rel; @@ -3934,7 +3934,7 @@ mod tests { #[test] fn test_empty_string() { let styles = parse_tailwind_to_styles(""); - assert!(styles.is_empty()); + assert_eq!(styles, vec![]); } #[test] From d6453d828267dc92c79c1ea624d5ac213cd31261 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sat, 3 Oct 2026 12:15:29 +0900 Subject: [PATCH 3/3] fix(components): provide Select and Stepper context as React 18 does Refs #691 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .changepacks/changepack_log_react_18_support.json | 2 +- packages/components/src/components/Select/index.tsx | 4 ++-- packages/components/src/components/Stepper/index.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.changepacks/changepack_log_react_18_support.json b/.changepacks/changepack_log_react_18_support.json index 8fbde2ca0..ad74861ef 100644 --- a/.changepacks/changepack_log_react_18_support.json +++ b/.changepacks/changepack_log_react_18_support.json @@ -3,6 +3,6 @@ "packages/react/package.json": "Patch", "packages/components/package.json": "Patch" }, - "note": "@devup-ui/react and @devup-ui/components take React as a peer of ^18 || ^19 instead of `*` and no longer depend on React 19 themselves, so a React 18 app no longer gets a second React. Select and Stepper read their context with useContext instead of React 19's use(), and Select repositions its container from an effect instead of a callback ref returning cleanup, which React 18 ignores", + "note": "@devup-ui/react and @devup-ui/components take React as a peer of ^18 || ^19 instead of `*` and no longer depend on React 19 themselves, so a React 18 app no longer gets a second React. Select and Stepper read their context with useContext instead of React 19's use() and provide it with Context.Provider instead of React 19's (which crashes React 18), and Select repositions its container from an effect instead of a callback ref returning cleanup, which React 18 ignores", "date": "2026-10-01T00:00:00.000Z" } diff --git a/packages/components/src/components/Select/index.tsx b/packages/components/src/components/Select/index.tsx index 7392925d5..6818bb5e5 100644 --- a/packages/components/src/components/Select/index.tsx +++ b/packages/components/src/components/Select/index.tsx @@ -110,7 +110,7 @@ export function Select({ } return ( - - + ) } diff --git a/packages/components/src/components/Stepper/index.tsx b/packages/components/src/components/Stepper/index.tsx index 229acf838..5fe1d2a3b 100644 --- a/packages/components/src/components/Stepper/index.tsx +++ b/packages/components/src/components/Stepper/index.tsx @@ -58,7 +58,7 @@ function Stepper({ } return ( - {children} - + ) }