diff --git a/.changeset/base-ui-1-8-0.md b/.changeset/base-ui-1-8-0.md new file mode 100644 index 000000000..788f954da --- /dev/null +++ b/.changeset/base-ui-1-8-0.md @@ -0,0 +1,5 @@ +--- +'@vapor-ui/core': patch +--- + +update `@base-ui/react` to 1.8.0 — support `keepMounted` on `Avatar` with stacked image/fallback styles, accept functional updater in Toast `update` diff --git a/apps/website/package.json b/apps/website/package.json index 9b589fbe1..3f44add82 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -14,7 +14,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@base-ui/react": "^1.7.0", + "@base-ui/react": "^1.8.0", "@radix-ui/react-collapsible": "^1.1.20", "@radix-ui/react-dialog": "^1.1.23", "@radix-ui/react-radio-group": "^1.4.7", diff --git a/apps/website/public/components/generated/avatar-image-primitive.json b/apps/website/public/components/generated/avatar-image-primitive.json index 3625bc223..d1bc11dbe 100644 --- a/apps/website/public/components/generated/avatar-image-primitive.json +++ b/apps/website/public/components/generated/avatar-image-primitive.json @@ -5,21 +5,36 @@ "props": [ { "name": "className", - "type": ["string", "(state: State) => string"], + "type": [ + "string", + "(state: State) => string" + ], "required": false, "description": "요소에 적용된 CSS 클래스 또는 컴포넌트의 상태에 따라 클래스를 반환하는 함수." }, { "name": "render", - "type": ["ReactElement"], + "type": [ + "ReactElement" + ], "required": false, "description": "컴포넌트의 HTML 요소를 다른 태그로 대체하거나 다른 컴포넌트와 조합할 수 있습니다. 렌더링할 요소를 반환하는 `ReactElement` 또는 함수를 인수로 받습니다." }, { "name": "onLoadingStatusChange", - "type": ["(status: ImageLoadingStatus) => void"], + "type": [ + "(status: ImageLoadingStatus) => void" + ], "required": false, "description": "로딩 상태가 변경될 때 호출되는 콜백" + }, + { + "name": "keepMounted", + "type": [ + "boolean" + ], + "required": false, + "description": "이미지 요소를 미리 로드하지 않고 마운트된 상태로 유지하며 제자리에서 로드합니다. `loading=\"lazy\"`와 `next/image` 같은 최적화 이미지 컴포넌트를 지원합니다. 기본값은 `false`입니다." } ] } diff --git a/packages/composites/package.json b/packages/composites/package.json index 6822771a8..a096c52e5 100644 --- a/packages/composites/package.json +++ b/packages/composites/package.json @@ -61,7 +61,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@base-ui/utils": "^0.3.2", + "@base-ui/utils": "^0.4.0", "@vapor-ui/icons": "workspace:^", "clsx": "^2.1.1" }, diff --git a/packages/core/package.json b/packages/core/package.json index e059e736f..4f3705f06 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -65,8 +65,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@base-ui/react": "^1.7.0", - "@base-ui/utils": "^0.3.2", + "@base-ui/react": "^1.8.0", + "@base-ui/utils": "^0.4.0", "@vanilla-extract/css": "^1.21.2", "@vanilla-extract/css-utils": "^0.1.6", "@vanilla-extract/dynamic": "^2.1.5", diff --git a/packages/core/src/components/avatar/avatar.css.ts b/packages/core/src/components/avatar/avatar.css.ts index dcf3448b0..ca61db2fa 100644 --- a/packages/core/src/components/avatar/avatar.css.ts +++ b/packages/core/src/components/avatar/avatar.css.ts @@ -8,6 +8,7 @@ const radii = createVar('border-radius'); export const root = componentRecipe({ base: { + position: 'relative', display: 'inline-flex', flexShrink: 0, alignItems: 'center', @@ -60,6 +61,8 @@ export const fallbackBgVar = createVar('fallback-background-color'); export const fallback = componentRecipe({ base: { + position: 'absolute', + inset: 0, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', @@ -105,10 +108,18 @@ export const fallback = componentRecipe({ }); export const image = componentStyle({ + position: 'absolute', + inset: 0, display: 'inline', objectFit: 'cover', width: '100%', height: '100%', + + selectors: { + '&[data-loading], &[data-error]': { + visibility: 'hidden', + }, + }, }); export type RootVariants = NonNullable>; diff --git a/packages/core/src/components/avatar/avatar.stories.tsx b/packages/core/src/components/avatar/avatar.stories.tsx index a8682a68d..cca5e33b9 100644 --- a/packages/core/src/components/avatar/avatar.stories.tsx +++ b/packages/core/src/components/avatar/avatar.stories.tsx @@ -70,3 +70,96 @@ export const TestBed: Story = { ), }; + +/* ------------------------------------------------------------------------------------------------- + * keepMounted — 스타일 검증용 + * -----------------------------------------------------------------------------------------------*/ + +const BROKEN_URL = 'https://example.invalid/not-an-image.png'; + +/** + * keepMounted를 켜면 로딩 중·에러 시 image와 fallback이 DOM에 공존한다. + * 현재 avatar.css.ts는 둘 다 positioned가 아니라 이 공존을 전제하지 않는다. + * A~D를 나란히 비교해 어떤 CSS가 필요한지 눈으로 확인하는 스토리. + */ +const STACK_STYLES = ` +.kmStack { position: relative; } +.kmStack > * { position: absolute; inset: 0; } +.kmHide[data-loading], +.kmHide[data-error] { visibility: hidden; } +`; + +const Row = ({ + label, + rootClassName, + imageClassName, + keepMounted, + delay, +}: { + label: string; + rootClassName?: string; + imageClassName?: string; + keepMounted?: boolean; + delay?: number; +}) => ( + + {label} + + {( + [ + ['loaded', IMAGE_URL], + ['error', BROKEN_URL], + ['no src', undefined], + ] as const + ).map(([state, src]) => ( + + + + + + {state} + + ))} + + +); + +export const KeepMounted: Story = { + render: () => ( + <> + + + + + + + + + + + ), +}; diff --git a/packages/core/src/components/avatar/avatar.tsx b/packages/core/src/components/avatar/avatar.tsx index ed03b7171..72508dfa3 100644 --- a/packages/core/src/components/avatar/avatar.tsx +++ b/packages/core/src/components/avatar/avatar.tsx @@ -46,6 +46,7 @@ export const AvatarRoot = forwardRef((props, 'height', 'loading', 'onLoadingStatusChange', + 'keepMounted', 'referrerPolicy', 'sizes', 'srcSet', @@ -108,6 +109,7 @@ export const AvatarImagePrimitive = forwardRef', () => { expect(screen.getByText(PANEL_LINK_1)).toBeInTheDocument(); }); - it('should focus the first item when the menu is opened via ArrowDown key', async () => { + it('should keep focus on the trigger when the menu is opened via ArrowDown key and move focus to the first item on Tab', async () => { const rendered = render(); const trigger = rendered.getByRole('button', { name: TRIGGER_1 }); @@ -191,6 +191,10 @@ describe('', () => { await userEvent.keyboard('[ArrowDown]'); expect(screen.getByText(PANEL_LINK_1)).toBeInTheDocument(); + expect(trigger).toHaveFocus(); + + await userEvent.tab(); + expect(screen.getByText(PANEL_LINK_1)).toHaveFocus(); }); diff --git a/packages/core/src/components/toast/toast.tsx b/packages/core/src/components/toast/toast.tsx index 7935bf910..1503e80b4 100644 --- a/packages/core/src/components/toast/toast.tsx +++ b/packages/core/src/components/toast/toast.tsx @@ -315,7 +315,7 @@ type ToastOptions = { icon?: ReactElement; close?: boolean; actionProps type ToastProps = ToastVariants & ToastOptions; -type BaseToastObject = Partial>; +type BaseToastObject = BaseToast.Root.ToastObject; type ToastObject = Omit, 'type' | 'actionProps'>; type ToastObjectType = ToastObject & ToastProps; @@ -332,7 +332,10 @@ export interface ToastManagerAddOptions extends Omit< } export interface ToastManagerUpdateOptions extends Partial< - ToastManagerAddOptions + Omit< + ToastObjectType, + 'id' | 'ref' | 'height' | 'transitionStatus' | 'limited' | 'updateKey' + > > {} export interface ToastManagerPromiseOptions extends BasePromiseOptions { @@ -355,7 +358,12 @@ export interface ToastManager extends BaseToastManager { }) => void, ) => () => void; add: (options: ToastManagerAddOptions) => string; - update: (id: string, options: ToastManagerUpdateOptions) => void; + update: ( + id: string, + options: + | ToastManagerUpdateOptions + | ((prevToast: ToastObjectType) => ToastManagerUpdateOptions), + ) => void; close: (id?: string) => void; promise: ( promise: Promise, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ec8ae51db..d36a1c60d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -242,8 +242,8 @@ importers: apps/website: dependencies: '@base-ui/react': - specifier: ^1.7.0 - version: 1.7.0(@date-fns/tz@1.4.1)(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: ^1.8.0 + version: 1.8.0(@date-fns/tz@1.4.1)(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@radix-ui/react-collapsible': specifier: ^1.1.20 version: 1.1.20(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -494,8 +494,8 @@ importers: packages/composites: dependencies: '@base-ui/utils': - specifier: ^0.3.2 - version: 0.3.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: ^0.4.0 + version: 0.4.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@vapor-ui/icons': specifier: workspace:^ version: link:../icons @@ -579,11 +579,11 @@ importers: packages/core: dependencies: '@base-ui/react': - specifier: ^1.7.0 - version: 1.7.0(@date-fns/tz@1.4.1)(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: ^1.8.0 + version: 1.8.0(@date-fns/tz@1.4.1)(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@base-ui/utils': - specifier: ^0.3.2 - version: 0.3.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + specifier: ^0.4.0 + version: 0.4.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@vanilla-extract/css': specifier: ^1.21.2 version: 1.21.2 @@ -1756,6 +1756,10 @@ packages: resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + '@babel/template@7.27.2': resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} @@ -1780,8 +1784,8 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@base-ui/react@1.7.0': - resolution: {integrity: sha512-j+8QjX44C32jrXD/qyEAGpFr70FRpGL2CY61mQd9nBPWN737CK0xxD1ceJ055rW4RtdvFDT1e7otzdlfxvsYug==} + '@base-ui/react@1.8.0': + resolution: {integrity: sha512-P0/1sxo6SBVZOklKMIedvTWqw2s2IQzi9x5bIVsXu980cuSOD4NeuRSs+/L7LZQfDkZP/uRZyGPyfFl/B1oH+Q==} engines: {node: '>=14.0.0'} peerDependencies: '@date-fns/tz': ^1.2.0 @@ -1797,8 +1801,8 @@ packages: date-fns: optional: true - '@base-ui/utils@0.3.2': - resolution: {integrity: sha512-oWy1aq/I2GmYjpl4PhEAhzflF8VPGKgZeq0xAWTbfD5KBWyxcN0ZP2+WHSUm/5Z6lVMBDLReLcoXwSYoRc/zNQ==} + '@base-ui/utils@0.4.0': + resolution: {integrity: sha512-bO9fz25kKtPf+aZVyfQrC0PDmJdmVni31W2hCS5/Owb+inwdIL3XU26pCPRPlt4LSxZrBgLwubXQXQlKaFEZzw==} peerDependencies: '@types/react': ^17 || ^18 || ^19 react: ^17 || ^18 || ^19 @@ -11428,6 +11432,8 @@ snapshots: '@babel/runtime@7.29.2': {} + '@babel/runtime@7.29.7': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -11474,10 +11480,10 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 - '@base-ui/react@1.7.0(@date-fns/tz@1.4.1)(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@base-ui/react@1.8.0(@date-fns/tz@1.4.1)(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@babel/runtime': 7.29.2 - '@base-ui/utils': 0.3.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@babel/runtime': 7.29.7 + '@base-ui/utils': 0.4.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@floating-ui/utils': 0.2.12 react: 19.2.8 @@ -11487,9 +11493,9 @@ snapshots: '@date-fns/tz': 1.4.1 '@types/react': 19.2.18 - '@base-ui/utils@0.3.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + '@base-ui/utils@0.4.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@babel/runtime': 7.29.2 + '@babel/runtime': 7.29.7 '@floating-ui/utils': 0.2.12 react: 19.2.8 react-dom: 19.2.8(react@19.2.8) @@ -12103,7 +12109,7 @@ snapshots: '@fumadocs/base-ui@16.15.5(@date-fns/tz@1.4.1)(@types/mdx@2.0.14)(@types/react@19.2.18)(fumadocs-core@16.15.5(@mdx-js/mdx@3.1.1)(@oramacloud/client@2.1.4)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.39.0(react@19.2.8))(next@16.2.12(@babel/core@7.28.4)(@playwright/test@1.62.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(sass@1.102.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(zod@4.4.3))(next@16.2.12(@babel/core@7.28.4)(@playwright/test@1.62.1)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(sass@1.102.0))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3)': dependencies: - '@base-ui/react': 1.7.0(@date-fns/tz@1.4.1)(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@base-ui/react': 1.8.0(@date-fns/tz@1.4.1)(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@fuma-translate/react': 1.0.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@fumadocs/tailwind': 0.1.1(tailwindcss@4.3.3) class-variance-authority: 0.7.1