From 67886fdfcbff53b29876cc186f2edb631b480428 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 12 Nov 2026 14:57:58 -0500 Subject: [PATCH 1/7] chore(mosaic): spike Avatar namespace export for RSC --- packages/mosaic/scripts/check-bundle.mjs | 17 +++++++++++++++-- .../src/components/avatar/avatar.parts.ts | 1 + .../src/components/avatar/avatar.test.tsx | 2 +- .../mosaic/src/components/avatar/avatar.tsx | 15 +++------------ packages/mosaic/src/components/avatar/index.ts | 4 +--- packages/mosaic/tsdown.config.mts | 1 + 6 files changed, 22 insertions(+), 18 deletions(-) create mode 100644 packages/mosaic/src/components/avatar/avatar.parts.ts diff --git a/packages/mosaic/scripts/check-bundle.mjs b/packages/mosaic/scripts/check-bundle.mjs index 8d911c6048f..a4a42c1dc93 100644 --- a/packages/mosaic/scripts/check-bundle.mjs +++ b/packages/mosaic/scripts/check-bundle.mjs @@ -8,9 +8,22 @@ const componentEntries = (await readdir(resolve(distDir, 'components'))) .filter(file => file.endsWith('.js')) .map(file => `components/${file}`); -for (const entryFile of ['index.js', ...componentEntries]) { +const isClientModule = source => source.startsWith("'use client';") || source.startsWith('"use client";'); +const namespaceEntries = { 'components/avatar.js': 'components/avatar/avatar.js' }; + +for (const [entryFile, leafFile] of Object.entries(namespaceEntries)) { + const entry = await readFile(resolve(distDir, entryFile), 'utf8'); + const leaf = await readFile(resolve(distDir, leafFile), 'utf8'); + if (isClientModule(entry) || !isClientModule(leaf)) { + throw new Error( + `Namespace entry ${entryFile} must have no directive, and its leaf ${leafFile} must be 'use client'.`, + ); + } +} + +for (const entryFile of ['index.js', ...componentEntries].filter(file => !(file in namespaceEntries))) { const entry = await readFile(resolve(distDir, entryFile), 'utf8'); - if (!entry.startsWith("'use client';") && !entry.startsWith('"use client";')) { + if (!isClientModule(entry)) { throw new Error(`Published Mosaic JS entry ${entryFile} is missing a top-level 'use client' boundary.`); } } diff --git a/packages/mosaic/src/components/avatar/avatar.parts.ts b/packages/mosaic/src/components/avatar/avatar.parts.ts new file mode 100644 index 00000000000..457cee70d83 --- /dev/null +++ b/packages/mosaic/src/components/avatar/avatar.parts.ts @@ -0,0 +1 @@ +export { AvatarRoot as Root, AvatarImage as Image, AvatarFallback as Fallback, AvatarIcon as Icon } from './avatar'; diff --git a/packages/mosaic/src/components/avatar/avatar.test.tsx b/packages/mosaic/src/components/avatar/avatar.test.tsx index 3a7a24996d8..890a314e1c4 100644 --- a/packages/mosaic/src/components/avatar/avatar.test.tsx +++ b/packages/mosaic/src/components/avatar/avatar.test.tsx @@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { Pen } from '../../icons/glyphs/pen'; import { Icon } from '../icon'; -import { Avatar } from './avatar'; +import { Avatar } from '.'; import { styles } from './avatar.styles'; // React reads this off the global object and ships no typing for it. diff --git a/packages/mosaic/src/components/avatar/avatar.tsx b/packages/mosaic/src/components/avatar/avatar.tsx index 5ea81429a84..13646707727 100644 --- a/packages/mosaic/src/components/avatar/avatar.tsx +++ b/packages/mosaic/src/components/avatar/avatar.tsx @@ -1,3 +1,5 @@ +'use client'; + import { useSafeLayoutEffect } from '@clerk/shared/react'; import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -193,15 +195,4 @@ const AvatarIcon = React.forwardRef(function M ); }); -/** - * Compound avatar. `Avatar.Root` positions and sizes the box; `Avatar.Image` renders - * once its source loads; `Avatar.Fallback` holds the space until then, as a blank - * placeholder that shimmers only while an image is actually on its way; `Avatar.Icon` - * adds an optional corner affordance. - */ -export const Avatar = { - Root: AvatarRoot, - Image: AvatarImage, - Fallback: AvatarFallback, - Icon: AvatarIcon, -}; +export { AvatarRoot, AvatarImage, AvatarFallback, AvatarIcon }; diff --git a/packages/mosaic/src/components/avatar/index.ts b/packages/mosaic/src/components/avatar/index.ts index d0536b3f373..189dd95c465 100644 --- a/packages/mosaic/src/components/avatar/index.ts +++ b/packages/mosaic/src/components/avatar/index.ts @@ -1,4 +1,2 @@ -'use client'; - -export { Avatar } from './avatar'; +export * as Avatar from './avatar.parts'; export type { AvatarProps, AvatarImageProps, AvatarFallbackProps, AvatarIconProps } from './avatar'; diff --git a/packages/mosaic/tsdown.config.mts b/packages/mosaic/tsdown.config.mts index c913e5a9b48..504fdb98b8e 100644 --- a/packages/mosaic/tsdown.config.mts +++ b/packages/mosaic/tsdown.config.mts @@ -37,6 +37,7 @@ export default defineConfig({ tsgo: { path: getTypeScriptExecutablePath() }, }, clean: true, + unbundle: true, target: 'es2022', platform: 'browser', minify: false, From 5a814fb531781777da256d1418cba11d18df0f8c Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 12 Nov 2026 15:09:44 -0500 Subject: [PATCH 2/7] feat(mosaic): render compound components from Server Components --- packages/mosaic/.gitignore | 3 + packages/mosaic/package.json | 2 + packages/mosaic/scripts/check-bundle.mjs | 64 ++++++-- packages/mosaic/scripts/test-rsc.mjs | 71 +++++++++ .../components/action-bar/action-bar.parts.ts | 7 + .../components/action-bar/action-bar.test.tsx | 2 +- .../src/components/action-bar/action-bar.tsx | 10 +- .../mosaic/src/components/action-bar/index.ts | 2 +- .../mosaic/src/components/avatar/index.ts | 6 + .../src/components/banner/banner.parts.ts | 1 + .../src/components/banner/banner.test.tsx | 2 +- .../mosaic/src/components/banner/banner.tsx | 18 +-- .../mosaic/src/components/banner/index.ts | 18 ++- .../src/components/card/card.browser.test.tsx | 2 +- .../mosaic/src/components/card/card.parts.ts | 10 ++ .../mosaic/src/components/card/card.test.tsx | 2 +- packages/mosaic/src/components/card/card.tsx | 25 ++- packages/mosaic/src/components/card/index.ts | 14 +- .../src/components/combobox/combobox.parts.ts | 12 ++ .../src/components/combobox/combobox.test.tsx | 2 +- .../src/components/combobox/combobox.tsx | 13 -- .../mosaic/src/components/combobox/index.ts | 2 +- .../components/data-list/data-list.parts.ts | 6 + .../components/data-list/data-list.test.tsx | 2 +- .../src/components/data-list/data-list.tsx | 12 +- .../mosaic/src/components/data-list/index.ts | 12 +- .../dialog/__tests__/alert-dialog.test.tsx | 2 +- .../dialog/__tests__/dialog.test.tsx | 2 +- .../__tests__/sheet-keyboard.webkit.test.tsx | 2 +- .../src/components/dialog/dialog.parts.ts | 8 + .../mosaic/src/components/dialog/dialog.tsx | 51 ++----- .../mosaic/src/components/dialog/index.ts | 35 ++++- .../src/components/drawer/drawer.parts.ts | 8 + .../src/components/drawer/drawer.test.tsx | 2 +- .../mosaic/src/components/drawer/drawer.tsx | 22 ++- .../mosaic/src/components/drawer/index.ts | 8 +- .../empty-state/empty-state.parts.ts | 7 + .../empty-state/empty-state.test.tsx | 2 +- .../components/empty-state/empty-state.tsx | 10 +- .../src/components/empty-state/index.ts | 4 +- .../components/entity/entity.browser.test.tsx | 2 +- .../src/components/entity/entity.parts.ts | 7 + .../src/components/entity/entity.test.tsx | 2 +- .../mosaic/src/components/entity/entity.tsx | 14 +- .../mosaic/src/components/entity/index.ts | 4 +- .../field/__tests__/field.ssr.test.tsx | 2 +- .../components/field/__tests__/field.test.tsx | 2 +- .../src/components/field/field.parts.ts | 11 ++ .../mosaic/src/components/field/field.tsx | 23 +-- packages/mosaic/src/components/field/index.ts | 3 +- .../mosaic/src/components/flow/flow.parts.ts | 1 + .../mosaic/src/components/flow/flow.test.tsx | 2 +- packages/mosaic/src/components/flow/flow.tsx | 5 +- packages/mosaic/src/components/flow/index.ts | 3 +- .../src/components/input-group/index.ts | 2 +- .../input-group/input-group.parts.ts | 6 + .../input-group/input-group.test.tsx | 2 +- .../components/input-group/input-group.tsx | 2 +- packages/mosaic/src/components/item/index.ts | 10 +- .../mosaic/src/components/item/item.parts.ts | 9 ++ .../mosaic/src/components/item/item.test.tsx | 2 +- packages/mosaic/src/components/item/item.tsx | 24 ++- packages/mosaic/src/components/menu/index.ts | 3 +- .../mosaic/src/components/menu/menu.parts.ts | 10 ++ .../mosaic/src/components/menu/menu.test.tsx | 2 +- packages/mosaic/src/components/menu/menu.tsx | 18 +-- packages/mosaic/src/components/panel/index.ts | 15 +- .../src/components/panel/panel.parts.ts | 1 + .../src/components/panel/panel.test.tsx | 2 +- .../mosaic/src/components/panel/panel.tsx | 16 +- .../mosaic/src/components/popover/index.ts | 3 +- .../src/components/popover/popover.parts.ts | 8 + .../src/components/popover/popover.test.tsx | 2 +- .../mosaic/src/components/popover/popover.tsx | 18 ++- .../mosaic/src/components/profile/index.ts | 17 ++- .../src/components/profile/profile.parts.ts | 9 ++ .../components/profile/profile.ssr.test.tsx | 2 +- .../src/components/profile/profile.test.tsx | 2 +- .../mosaic/src/components/profile/profile.tsx | 27 ++-- packages/mosaic/src/components/radio/index.ts | 2 +- .../src/components/radio/radio.parts.ts | 10 ++ .../src/components/radio/radio.test-d.tsx | 2 +- .../src/components/radio/radio.test.tsx | 2 +- .../mosaic/src/components/radio/radio.tsx | 17 +-- .../mosaic/src/components/section/index.ts | 7 +- .../src/components/section/section.parts.ts | 17 +++ .../src/components/section/section.test.tsx | 2 +- .../mosaic/src/components/section/section.tsx | 39 +++-- .../src/components/segmented-control/index.ts | 2 +- .../segmented-control.parts.ts | 1 + .../segmented-control.test-d.tsx | 2 +- .../segmented-control.test.tsx | 2 +- .../segmented-control/segmented-control.tsx | 7 +- .../mosaic/src/components/select/index.ts | 2 +- .../src/components/select/select.parts.ts | 10 ++ .../src/components/select/select.test-d.tsx | 2 +- .../src/components/select/select.test.tsx | 2 +- .../mosaic/src/components/select/select.tsx | 13 +- packages/mosaic/src/components/table/index.ts | 2 +- .../components/table/table.feature.test.tsx | 2 +- .../src/components/table/table.parts.ts | 13 ++ .../src/components/table/table.test.tsx | 2 +- .../mosaic/src/components/table/table.tsx | 26 ++-- packages/mosaic/src/components/tabs/index.ts | 2 +- .../mosaic/src/components/tabs/tabs.parts.ts | 8 + .../mosaic/src/components/tabs/tabs.test.tsx | 2 +- packages/mosaic/src/components/tabs/tabs.tsx | 16 +- .../mosaic/src/components/tooltip/index.ts | 2 +- .../src/components/tooltip/tooltip.parts.ts | 6 + .../src/components/tooltip/tooltip.test.tsx | 2 +- .../mosaic/src/components/tooltip/tooltip.tsx | 11 +- packages/mosaic/test/rsc/app/layout.jsx | 7 + packages/mosaic/test/rsc/app/unrendered.jsx | 5 + pnpm-lock.yaml | 143 +++++++++++++++++- 114 files changed, 819 insertions(+), 347 deletions(-) create mode 100644 packages/mosaic/scripts/test-rsc.mjs create mode 100644 packages/mosaic/src/components/action-bar/action-bar.parts.ts create mode 100644 packages/mosaic/src/components/banner/banner.parts.ts create mode 100644 packages/mosaic/src/components/card/card.parts.ts create mode 100644 packages/mosaic/src/components/combobox/combobox.parts.ts create mode 100644 packages/mosaic/src/components/data-list/data-list.parts.ts create mode 100644 packages/mosaic/src/components/dialog/dialog.parts.ts create mode 100644 packages/mosaic/src/components/drawer/drawer.parts.ts create mode 100644 packages/mosaic/src/components/empty-state/empty-state.parts.ts create mode 100644 packages/mosaic/src/components/entity/entity.parts.ts create mode 100644 packages/mosaic/src/components/field/field.parts.ts create mode 100644 packages/mosaic/src/components/flow/flow.parts.ts create mode 100644 packages/mosaic/src/components/input-group/input-group.parts.ts create mode 100644 packages/mosaic/src/components/item/item.parts.ts create mode 100644 packages/mosaic/src/components/menu/menu.parts.ts create mode 100644 packages/mosaic/src/components/panel/panel.parts.ts create mode 100644 packages/mosaic/src/components/popover/popover.parts.ts create mode 100644 packages/mosaic/src/components/profile/profile.parts.ts create mode 100644 packages/mosaic/src/components/radio/radio.parts.ts create mode 100644 packages/mosaic/src/components/section/section.parts.ts create mode 100644 packages/mosaic/src/components/segmented-control/segmented-control.parts.ts create mode 100644 packages/mosaic/src/components/select/select.parts.ts create mode 100644 packages/mosaic/src/components/table/table.parts.ts create mode 100644 packages/mosaic/src/components/tabs/tabs.parts.ts create mode 100644 packages/mosaic/src/components/tooltip/tooltip.parts.ts create mode 100644 packages/mosaic/test/rsc/app/layout.jsx create mode 100644 packages/mosaic/test/rsc/app/unrendered.jsx diff --git a/packages/mosaic/.gitignore b/packages/mosaic/.gitignore index 21c3f208e04..9b1788f5db0 100644 --- a/packages/mosaic/.gitignore +++ b/packages/mosaic/.gitignore @@ -1 +1,4 @@ .verify +test/rsc/.next +test/rsc/app/page.jsx +test/rsc/next-env.d.ts diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index cbdceb37b72..f41050837f5 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -132,6 +132,7 @@ "test:browser": "vitest run --project browser", "test:ci": "vitest run --project primitives --project mosaic --maxWorkers=70%", "test:feature": "vitest run --project feature", + "test:rsc": "node ./scripts/test-rsc.mjs", "test:visual": "bash ./scripts/test-visual.sh compare", "test:visual:update": "bash ./scripts/test-visual.sh update", "test:webkit": "vitest run --project webkit", @@ -160,6 +161,7 @@ "eslint-plugin-react-you-might-not-need-an-effect": "1.0.2", "happy-dom": "^20.8.9", "msw": "2.15.0", + "next": "^16.4.3", "playwright": "1.62.1", "qrcode.react": "4.2.0", "react": "catalog:react", diff --git a/packages/mosaic/scripts/check-bundle.mjs b/packages/mosaic/scripts/check-bundle.mjs index a4a42c1dc93..683787d7e28 100644 --- a/packages/mosaic/scripts/check-bundle.mjs +++ b/packages/mosaic/scripts/check-bundle.mjs @@ -1,30 +1,64 @@ import { readdir, readFile } from 'node:fs/promises'; -import { resolve } from 'node:path'; +import { dirname, relative, resolve } from 'node:path'; const distDir = resolve(import.meta.dirname, '../dist'); const jsFiles = (await readdir(distDir, { recursive: true })).filter(file => file.endsWith('.js')); const sources = await Promise.all(jsFiles.map(file => readFile(resolve(distDir, file), 'utf8'))); -const componentEntries = (await readdir(resolve(distDir, 'components'))) - .filter(file => file.endsWith('.js')) - .map(file => `components/${file}`); +const packageJson = JSON.parse(await readFile(resolve(import.meta.dirname, '../package.json'), 'utf8')); +const entryFiles = Object.entries(packageJson.exports) + .filter(([subpath]) => subpath === '.' || /^\.\/(components|primitives)\//.test(subpath)) + .map(([, target]) => target) + .map(target => (typeof target === 'string' ? target : target.import)) + .filter(file => file?.endsWith('.js')) + .map(file => resolve(import.meta.dirname, '..', file)); const isClientModule = source => source.startsWith("'use client';") || source.startsWith('"use client";'); -const namespaceEntries = { 'components/avatar.js': 'components/avatar/avatar.js' }; +const importsOf = source => + [...source.matchAll(/^import\s+(?:[^'"]*?\s+from\s+)?["']([^"']+)["'];?$/gm)].map(m => m[1]); +const importsReact = source => + importsOf(source).some(specifier => specifier === 'react' || specifier.startsWith('react/')); +const code = source => + source + .replace(/\/\/.*$/gm, '') + .split('\n') + .map(line => line.trim()) + .filter(Boolean); -for (const [entryFile, leafFile] of Object.entries(namespaceEntries)) { - const entry = await readFile(resolve(distDir, entryFile), 'utf8'); - const leaf = await readFile(resolve(distDir, leafFile), 'utf8'); - if (isClientModule(entry) || !isClientModule(leaf)) { +const checkNamespaceEntry = async (entryFile, source) => { + const display = relative(distDir, entryFile); + const lines = code(source); + const namespaceImport = /^import \{ (\w+) \} from "(\.[^"]+)";$/; + const namespaceExport = /^export \{ (\w+ as \w+(?:, \w+ as \w+)*) \};$/; + if (!lines.every(line => namespaceImport.test(line) || namespaceExport.test(line))) { throw new Error( - `Namespace entry ${entryFile} must have no directive, and its leaf ${leafFile} must be 'use client'.`, + `Published Mosaic JS entry ${display} must start with 'use client' or contain only \`export * as\` namespace re-exports.`, ); } -} + for (const line of lines.filter(line => namespaceImport.test(line))) { + const [, binding, specifier] = line.match(namespaceImport); + const partsFile = resolve(dirname(entryFile), specifier); + const parts = await readFile(partsFile, 'utf8'); + if (isClientModule(parts) || !new RegExp(`^var ${binding} = /\\* @__PURE__ \\*/ __exportAll\\(`, 'm').test(parts)) { + throw new Error( + `${display} imports ${binding} from ${relative(distDir, partsFile)}, which must be a namespace object with no directive.`, + ); + } + for (const leafSpecifier of importsOf(parts).filter(specifier => specifier.startsWith('.'))) { + const leafFile = resolve(dirname(partsFile), leafSpecifier); + const leaf = await readFile(leafFile, 'utf8'); + if (importsReact(leaf) && !isClientModule(leaf)) { + throw new Error( + `${relative(distDir, leafFile)}, re-exported by the namespace ${relative(distDir, partsFile)}, imports React but is missing a top-level 'use client' boundary.`, + ); + } + } + } +}; -for (const entryFile of ['index.js', ...componentEntries].filter(file => !(file in namespaceEntries))) { - const entry = await readFile(resolve(distDir, entryFile), 'utf8'); - if (!isClientModule(entry)) { - throw new Error(`Published Mosaic JS entry ${entryFile} is missing a top-level 'use client' boundary.`); +for (const entryFile of entryFiles) { + const source = await readFile(entryFile, 'utf8'); + if (!isClientModule(source)) { + await checkNamespaceEntry(entryFile, source); } } diff --git a/packages/mosaic/scripts/test-rsc.mjs b/packages/mosaic/scripts/test-rsc.mjs new file mode 100644 index 00000000000..f00dd11515a --- /dev/null +++ b/packages/mosaic/scripts/test-rsc.mjs @@ -0,0 +1,71 @@ +import { spawnSync } from 'node:child_process'; +import { readFile, rm, writeFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { resolve } from 'node:path'; + +const packageDir = resolve(import.meta.dirname, '..'); +const fixtureDir = resolve(packageDir, 'test/rsc'); +const packageJson = JSON.parse(await readFile(resolve(packageDir, 'package.json'), 'utf8')); + +const isNamespace = value => value !== null && typeof value === 'object' && !('$$typeof' in value); +const isComponent = (name, value) => + /^[A-Z]/.test(name) && + (typeof value === 'function' || + (value !== null && typeof value === 'object' && value.$$typeof !== Symbol.for('react.context'))); + +const entries = await Promise.all( + Object.entries(packageJson.exports) + .filter(([subpath]) => subpath.startsWith('./components/')) + .map(async ([subpath, target], index) => { + const module = await import(resolve(packageDir, target.import)); + return { specifier: `@clerk/mosaic${subpath.slice(1)}`, alias: `entry${index}`, module }; + }), +); + +const references = []; +const elements = []; +for (const { specifier, alias, module } of entries) { + for (const [name, value] of Object.entries(module)) { + if (isNamespace(value)) { + for (const part of Object.keys(value)) { + references.push(` ['${specifier}', '${name}.${part}', ${alias}.${name}.${part}],`); + if (isComponent(part, value[part])) elements.push(` <${alias}.${name}.${part} />`); + } + } else { + references.push(` ['${specifier}', '${name}', ${alias}.${name}],`); + if (isComponent(name, value)) elements.push(` <${alias}.${name} />`); + } + } +} + +const page = `import { Unrendered } from './unrendered'; +${entries.map(({ specifier, alias }) => `import * as ${alias} from '${specifier}';`).join('\n')} + +const references = [ +${references.join('\n')} +]; + +export default function Page() { + const missing = references.filter(([, , value]) => value === undefined).map(([specifier, name]) => \`\${specifier}: \${name}\`); + if (missing.length > 0) { + throw new Error(\`Undefined in a Server Component:\\n\${missing.join('\\n')}\`); + } + return ( + +${elements.join('\n')} + + ); +} +`; + +await writeFile(resolve(fixtureDir, 'app/page.jsx'), page); +await rm(resolve(fixtureDir, '.next'), { recursive: true, force: true }); + +const bundlerFlag = process.argv.includes('--webpack') ? ['--webpack'] : []; +const nextBin = createRequire(import.meta.url).resolve('next/dist/bin/next'); +const { status, error } = spawnSync(process.execPath, [nextBin, 'build', ...bundlerFlag, fixtureDir], { + cwd: packageDir, + stdio: 'inherit', +}); +if (error) throw error; +process.exit(status ?? 1); diff --git a/packages/mosaic/src/components/action-bar/action-bar.parts.ts b/packages/mosaic/src/components/action-bar/action-bar.parts.ts new file mode 100644 index 00000000000..24df04f6ea6 --- /dev/null +++ b/packages/mosaic/src/components/action-bar/action-bar.parts.ts @@ -0,0 +1,7 @@ +export { + ActionBarRoot as Root, + ActionBarAction as Action, + ActionBarCount as Count, + ActionBarSeparator as Separator, + ActionBarDismiss as Dismiss, +} from './action-bar'; diff --git a/packages/mosaic/src/components/action-bar/action-bar.test.tsx b/packages/mosaic/src/components/action-bar/action-bar.test.tsx index 89c4d412103..e6941f828a5 100644 --- a/packages/mosaic/src/components/action-bar/action-bar.test.tsx +++ b/packages/mosaic/src/components/action-bar/action-bar.test.tsx @@ -7,7 +7,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { PortalProvider } from '../../primitives/portal'; import { Dialog } from '../dialog'; import { Menu } from '../menu'; -import { ActionBar } from './action-bar'; +import { ActionBar } from '.'; const testStyles = stylex.create({ positioner: { diff --git a/packages/mosaic/src/components/action-bar/action-bar.tsx b/packages/mosaic/src/components/action-bar/action-bar.tsx index 1487e528384..7e0a80c1bf3 100644 --- a/packages/mosaic/src/components/action-bar/action-bar.tsx +++ b/packages/mosaic/src/components/action-bar/action-bar.tsx @@ -1,3 +1,5 @@ +'use client'; + import { autoUpdate, FloatingFocusManager, @@ -343,4 +345,10 @@ const Dismiss = React.forwardRef(funct ); }); -export const ActionBar = { Root, Action, Count, Separator, Dismiss }; +export { + Root as ActionBarRoot, + Action as ActionBarAction, + Count as ActionBarCount, + Separator as ActionBarSeparator, + Dismiss as ActionBarDismiss, +}; diff --git a/packages/mosaic/src/components/action-bar/index.ts b/packages/mosaic/src/components/action-bar/index.ts index 4008e2405a7..5c9565a33e8 100644 --- a/packages/mosaic/src/components/action-bar/index.ts +++ b/packages/mosaic/src/components/action-bar/index.ts @@ -1,4 +1,4 @@ -export { ActionBar } from './action-bar'; +export * as ActionBar from './action-bar.parts'; export type { ActionBarActionProps, ActionBarCountProps, diff --git a/packages/mosaic/src/components/avatar/index.ts b/packages/mosaic/src/components/avatar/index.ts index 189dd95c465..5a6816e7220 100644 --- a/packages/mosaic/src/components/avatar/index.ts +++ b/packages/mosaic/src/components/avatar/index.ts @@ -1,2 +1,8 @@ +/** + * Compound avatar. `Avatar.Root` positions and sizes the box; `Avatar.Image` renders + * once its source loads; `Avatar.Fallback` holds the space until then, as a blank + * placeholder that shimmers only while an image is actually on its way; `Avatar.Icon` + * adds an optional corner affordance. + */ export * as Avatar from './avatar.parts'; export type { AvatarProps, AvatarImageProps, AvatarFallbackProps, AvatarIconProps } from './avatar'; diff --git a/packages/mosaic/src/components/banner/banner.parts.ts b/packages/mosaic/src/components/banner/banner.parts.ts new file mode 100644 index 00000000000..d8997d344b0 --- /dev/null +++ b/packages/mosaic/src/components/banner/banner.parts.ts @@ -0,0 +1 @@ +export { BannerRoot as Root, BannerLabel as Label, BannerDescription as Description } from './banner'; diff --git a/packages/mosaic/src/components/banner/banner.test.tsx b/packages/mosaic/src/components/banner/banner.test.tsx index 27b3d7233d3..4e6832a081f 100644 --- a/packages/mosaic/src/components/banner/banner.test.tsx +++ b/packages/mosaic/src/components/banner/banner.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { describe, expect, it } from 'vitest'; -import { Banner } from './banner'; +import { Banner } from '.'; const overrides = stylex.create({ root: { marginTop: '8px' }, diff --git a/packages/mosaic/src/components/banner/banner.tsx b/packages/mosaic/src/components/banner/banner.tsx index 2df765062fa..2a839e839df 100644 --- a/packages/mosaic/src/components/banner/banner.tsx +++ b/packages/mosaic/src/components/banner/banner.tsx @@ -1,3 +1,5 @@ +'use client'; + import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -104,18 +106,4 @@ const Description = React.forwardRef - * Error banner - * Renew now to avoid service interruption. - * - */ -export const Banner = { Root, Label, Description }; +export { Root as BannerRoot, Label as BannerLabel, Description as BannerDescription }; diff --git a/packages/mosaic/src/components/banner/index.ts b/packages/mosaic/src/components/banner/index.ts index a82427a74ca..414f3f0cd37 100644 --- a/packages/mosaic/src/components/banner/index.ts +++ b/packages/mosaic/src/components/banner/index.ts @@ -1,4 +1,16 @@ -'use client'; - -export { Banner } from './banner'; +/** + * A tinted strip that annotates the surface it sits on with a status message. Composed through + * `Banner.Root`, `Banner.Label`, and `Banner.Description`; every part accepts the Mosaic `render` + * prop and forwards its ref. `Banner.Root` renders the icon for its `color` itself. + * + * `Banner.Root` sets no ARIA role. A banner that appears in response to something the user did + * should be given `role='status'` (or `role='alert'` for an error) so it is announced. + * + * @example + * + * Error banner + * Renew now to avoid service interruption. + * + */ +export * as Banner from './banner.parts'; export type { BannerDescriptionProps, BannerLabelProps, BannerRootProps } from './banner'; diff --git a/packages/mosaic/src/components/card/card.browser.test.tsx b/packages/mosaic/src/components/card/card.browser.test.tsx index c3a4e808cc7..e30aeb86577 100644 --- a/packages/mosaic/src/components/card/card.browser.test.tsx +++ b/packages/mosaic/src/components/card/card.browser.test.tsx @@ -4,7 +4,7 @@ import { afterEach, describe, expect, it } from 'vitest'; import { Dialog } from '../dialog'; import { Flow } from '../flow'; -import { Card } from './card'; +import { Card } from '.'; afterEach(() => cleanup()); diff --git a/packages/mosaic/src/components/card/card.parts.ts b/packages/mosaic/src/components/card/card.parts.ts new file mode 100644 index 00000000000..85633f32b99 --- /dev/null +++ b/packages/mosaic/src/components/card/card.parts.ts @@ -0,0 +1,10 @@ +export { + CardRoot as Root, + CardHeader as Header, + CardMedia as Media, + CardTitle as Title, + CardDescription as Description, + CardBanner as Banner, + CardContent as Content, + CardFooter as Footer, +} from './card'; diff --git a/packages/mosaic/src/components/card/card.test.tsx b/packages/mosaic/src/components/card/card.test.tsx index 7d9bc9953de..54fe8f94e27 100644 --- a/packages/mosaic/src/components/card/card.test.tsx +++ b/packages/mosaic/src/components/card/card.test.tsx @@ -8,7 +8,7 @@ import { Button, SubmitButton } from '../button'; import { ButtonContext } from '../button/button.context'; import { Dialog } from '../dialog'; import { Flow } from '../flow'; -import { Card } from './card'; +import { Card } from '.'; const compactCard = '@container card (max-width: 20rem)' as const; diff --git a/packages/mosaic/src/components/card/card.tsx b/packages/mosaic/src/components/card/card.tsx index 2114de982d3..e930b898cc5 100644 --- a/packages/mosaic/src/components/card/card.tsx +++ b/packages/mosaic/src/components/card/card.tsx @@ -1,3 +1,5 @@ +'use client'; + import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -318,16 +320,13 @@ const Footer = React.forwardRef>(fun return {element}; }); -/** - * A styled surface composed through `Card.Root`, `Card.Header`, `Card.Media`, `Card.Title`, - * `Card.Description`, `Card.Banner`, `Card.Content`, and `Card.Footer`. Every part accepts the Mosaic - * `render` prop and forwards its ref. - * - * `Card.Banner` is a message slot between the header and the content. It is always rendered, and - * a `Banner` expands into it while its children hold a message, collapsing again once they are - * empty, so the slot takes `role='alert'` and stays a live region across both. - * - * Rendered as the content of a `Dialog.Popup`, the card reads that surface from `DialogContext`: - * the title and description take the popup's ARIA ids, and the header carries the dismiss button. - */ -export const Card = { Root, Header, Media, Title, Description, Banner: CardBanner, Content, Footer }; +export { + Root as CardRoot, + Header as CardHeader, + Media as CardMedia, + Title as CardTitle, + Description as CardDescription, + CardBanner, + Content as CardContent, + Footer as CardFooter, +}; diff --git a/packages/mosaic/src/components/card/index.ts b/packages/mosaic/src/components/card/index.ts index bbb91c01e4b..b78516bb3e4 100644 --- a/packages/mosaic/src/components/card/index.ts +++ b/packages/mosaic/src/components/card/index.ts @@ -1,2 +1,14 @@ -export { Card } from './card'; +/** + * A styled surface composed through `Card.Root`, `Card.Header`, `Card.Media`, `Card.Title`, + * `Card.Description`, `Card.Banner`, `Card.Content`, and `Card.Footer`. Every part accepts the Mosaic + * `render` prop and forwards its ref. + * + * `Card.Banner` is a message slot between the header and the content. It is always rendered, and + * a `Banner` expands into it while its children hold a message, collapsing again once they are + * empty, so the slot takes `role='alert'` and stays a live region across both. + * + * Rendered as the content of a `Dialog.Popup`, the card reads that surface from `DialogContext`: + * the title and description take the popup's ARIA ids, and the header carries the dismiss button. + */ +export * as Card from './card.parts'; export type { CardAlign, CardBannerProps, CardProps, CardSize } from './card'; diff --git a/packages/mosaic/src/components/combobox/combobox.parts.ts b/packages/mosaic/src/components/combobox/combobox.parts.ts new file mode 100644 index 00000000000..f41d12585ef --- /dev/null +++ b/packages/mosaic/src/components/combobox/combobox.parts.ts @@ -0,0 +1,12 @@ +export { + ComboboxRoot as Root, + ComboboxCollection as Collection, + ComboboxInput as Input, + ComboboxTrigger as Trigger, + ComboboxClear as Clear, + ComboboxPopup as Popup, + ComboboxList as List, + ComboboxOption as Option, + ComboboxOptionIndicator as OptionIndicator, + ComboboxEmpty as Empty, +} from './combobox'; diff --git a/packages/mosaic/src/components/combobox/combobox.test.tsx b/packages/mosaic/src/components/combobox/combobox.test.tsx index 1fa8bf124d5..9b3c6a7ed99 100644 --- a/packages/mosaic/src/components/combobox/combobox.test.tsx +++ b/packages/mosaic/src/components/combobox/combobox.test.tsx @@ -9,7 +9,7 @@ import { Button } from '../button'; import { Field } from '../field'; import { Icon } from '../icon'; import { InputGroup } from '../input-group'; -import { Combobox } from './combobox'; +import { Combobox } from '.'; function FloatingCombobox(props?: { onValueChange?: (value: string | null) => void; anchor?: HTMLElement }) { return ( diff --git a/packages/mosaic/src/components/combobox/combobox.tsx b/packages/mosaic/src/components/combobox/combobox.tsx index 48a510851b1..e3ac25e4206 100644 --- a/packages/mosaic/src/components/combobox/combobox.tsx +++ b/packages/mosaic/src/components/combobox/combobox.tsx @@ -248,16 +248,3 @@ export const ComboboxEmpty = React.forwardRef>(function }); }); -/** - * Mosaic `DataList` — label/value pairs for read-only detail, such as the fields describing a - * device or a payment method. Composed via dot syntax: `DataList.Root`, `DataList.Item`, - * `DataList.Label`, `DataList.Value`. Every part takes a `render` prop and forwards a ref. - * - * Semantics come free: the parts render `dl`/`dt`/`dd`, so assistive technology reads each value - * with the label it belongs to. - */ -export const DataList = { Root, Item, Label, Value }; +export { Root as DataListRoot, Item as DataListItem, Label as DataListLabel, Value as DataListValue }; diff --git a/packages/mosaic/src/components/data-list/index.ts b/packages/mosaic/src/components/data-list/index.ts index 62a63565020..6e2d88f1ff6 100644 --- a/packages/mosaic/src/components/data-list/index.ts +++ b/packages/mosaic/src/components/data-list/index.ts @@ -1,4 +1,10 @@ -'use client'; - -export { DataList } from './data-list'; +/** + * Mosaic `DataList` — label/value pairs for read-only detail, such as the fields describing a + * device or a payment method. Composed via dot syntax: `DataList.Root`, `DataList.Item`, + * `DataList.Label`, `DataList.Value`. Every part takes a `render` prop and forwards a ref. + * + * Semantics come free: the parts render `dl`/`dt`/`dd`, so assistive technology reads each value + * with the label it belongs to. + */ +export * as DataList from './data-list.parts'; export type { DataListProps } from './data-list'; diff --git a/packages/mosaic/src/components/dialog/__tests__/alert-dialog.test.tsx b/packages/mosaic/src/components/dialog/__tests__/alert-dialog.test.tsx index c424c3101da..00bbba5a3ec 100644 --- a/packages/mosaic/src/components/dialog/__tests__/alert-dialog.test.tsx +++ b/packages/mosaic/src/components/dialog/__tests__/alert-dialog.test.tsx @@ -5,7 +5,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { Card } from '../../card'; import type { DialogRootProps } from '../dialog'; -import { Dialog } from '../dialog'; +import { Dialog } from '..'; afterEach(() => cleanup()); diff --git a/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx b/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx index af6605da7f1..667752af551 100644 --- a/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx +++ b/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx @@ -10,7 +10,7 @@ import { Button } from '../../button'; import { ButtonContext } from '../../button/button.context'; import { Card } from '../../card'; import type { DialogVariant } from '../dialog'; -import { Dialog } from '../dialog'; +import { Dialog } from '..'; import { styles as dialogStyles } from '../dialog.styles'; afterEach(() => cleanup()); diff --git a/packages/mosaic/src/components/dialog/__tests__/sheet-keyboard.webkit.test.tsx b/packages/mosaic/src/components/dialog/__tests__/sheet-keyboard.webkit.test.tsx index 420604a7d84..3b033a71d43 100644 --- a/packages/mosaic/src/components/dialog/__tests__/sheet-keyboard.webkit.test.tsx +++ b/packages/mosaic/src/components/dialog/__tests__/sheet-keyboard.webkit.test.tsx @@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { page } from 'vitest/browser'; import { Card } from '../../card'; -import { Dialog } from '../dialog'; +import { Dialog } from '..'; const KEYBOARD = 336; diff --git a/packages/mosaic/src/components/dialog/dialog.parts.ts b/packages/mosaic/src/components/dialog/dialog.parts.ts new file mode 100644 index 00000000000..192fb65d48c --- /dev/null +++ b/packages/mosaic/src/components/dialog/dialog.parts.ts @@ -0,0 +1,8 @@ +export { + DialogRoot as Root, + DialogTrigger as Trigger, + DialogPopup as Popup, + DialogClose as Close, + DialogCloseButton as CloseButton, + createDialogHandle as createHandle, +} from './dialog'; diff --git a/packages/mosaic/src/components/dialog/dialog.tsx b/packages/mosaic/src/components/dialog/dialog.tsx index 15d20b11af0..cb0ade6efe3 100644 --- a/packages/mosaic/src/components/dialog/dialog.tsx +++ b/packages/mosaic/src/components/dialog/dialog.tsx @@ -1,3 +1,5 @@ +'use client'; + import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -485,44 +487,13 @@ const Popup = React.forwardRef(function Dialog ); }); -/** - * Mosaic `Dialog` — a modal surface built on the `primitives/dialog` primitive, composed - * via dot syntax: - * - * ```tsx - * - * }>Open - * - * - * - * … - * … - * - * - * - * - * ``` - * - * The dialog brings the geometry, the motion and the floating tree; the SURFACE comes from what - * is rendered inside it — a `Card` for a `card`, a `Profile` for a `profile`. Those surfaces read - * `DialogContext`, so `Card.Title` names the dialog and `Card.Header` carries its dismiss without - * either knowing it is in one. - * - * `Dialog.Popup` renders the portal, the scrim and the centering viewport itself, so those are - * not parts. `role='alertdialog'` on the root makes it an alert dialog — one that interrupts to - * ask for a decision and waits for one. In the compact band a card is a bottom sheet unless - * `compactPlacement='center'`. - * - * Each styled part spreads `themeProps` + `stylex.props` through `mergeStyleProps`, so it - * carries the public `.cl-` class and StyleX atoms while the headless part keeps its focus - * management, scroll lock, and ARIA wiring. - */ -export const Dialog = { - Root, - Trigger, - Popup, - Close, - CloseButton, - /** Creates a handle linking detached `Dialog.Trigger`s to a `Dialog.Root` anywhere in the tree. */ - createHandle: Primitive.createHandle, +/** Creates a handle linking detached `Dialog.Trigger`s to a `Dialog.Root` anywhere in the tree. */ +const createDialogHandle = Primitive.createHandle; +export { + Root as DialogRoot, + Trigger as DialogTrigger, + Popup as DialogPopup, + Close as DialogClose, + CloseButton as DialogCloseButton, + createDialogHandle, }; diff --git a/packages/mosaic/src/components/dialog/index.ts b/packages/mosaic/src/components/dialog/index.ts index e2cec98bf11..de900f06ef3 100644 --- a/packages/mosaic/src/components/dialog/index.ts +++ b/packages/mosaic/src/components/dialog/index.ts @@ -1,4 +1,37 @@ -export { Dialog, DialogContext, isInDialog } from './dialog'; +/** + * Mosaic `Dialog` — a modal surface built on the `primitives/dialog` primitive, composed + * via dot syntax: + * + * ```tsx + * + * }>Open + * + * + * + * … + * … + * + * + * + * + * ``` + * + * The dialog brings the geometry, the motion and the floating tree; the SURFACE comes from what + * is rendered inside it — a `Card` for a `card`, a `Profile` for a `profile`. Those surfaces read + * `DialogContext`, so `Card.Title` names the dialog and `Card.Header` carries its dismiss without + * either knowing it is in one. + * + * `Dialog.Popup` renders the portal, the scrim and the centering viewport itself, so those are + * not parts. `role='alertdialog'` on the root makes it an alert dialog — one that interrupts to + * ask for a decision and waits for one. In the compact band a card is a bottom sheet unless + * `compactPlacement='center'`. + * + * Each styled part spreads `themeProps` + `stylex.props` through `mergeStyleProps`, so it + * carries the public `.cl-` class and StyleX atoms while the headless part keeps its focus + * management, scroll lock, and ARIA wiring. + */ +export * as Dialog from './dialog.parts'; +export { DialogContext, isInDialog } from './dialog'; export type { DialogFocusTarget, DialogHandle, DialogOpenChangeDetails, DialogRole } from '../../primitives/dialog'; export type { DialogCloseButtonProps, diff --git a/packages/mosaic/src/components/drawer/drawer.parts.ts b/packages/mosaic/src/components/drawer/drawer.parts.ts new file mode 100644 index 00000000000..7a1c89e09a2 --- /dev/null +++ b/packages/mosaic/src/components/drawer/drawer.parts.ts @@ -0,0 +1,8 @@ +export { + DrawerRoot as Root, + DrawerPopup as Popup, + DrawerTrigger as Trigger, + DrawerTitle as Title, + DrawerDescription as Description, + DrawerClose as Close, +} from './drawer'; diff --git a/packages/mosaic/src/components/drawer/drawer.test.tsx b/packages/mosaic/src/components/drawer/drawer.test.tsx index 344a81f29b0..43360682ddc 100644 --- a/packages/mosaic/src/components/drawer/drawer.test.tsx +++ b/packages/mosaic/src/components/drawer/drawer.test.tsx @@ -5,7 +5,7 @@ import { describe, expect, it } from 'vitest'; import { MosaicProvider } from '../../mosaic-provider'; import { Dialog } from '../dialog'; -import { Drawer } from './drawer'; +import { Drawer } from '.'; function Sheet({ defaultOpen = true }: { defaultOpen?: boolean }) { return ( diff --git a/packages/mosaic/src/components/drawer/drawer.tsx b/packages/mosaic/src/components/drawer/drawer.tsx index eae62f210d3..721b995ce44 100644 --- a/packages/mosaic/src/components/drawer/drawer.tsx +++ b/packages/mosaic/src/components/drawer/drawer.tsx @@ -1,3 +1,5 @@ +'use client'; + import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -118,17 +120,11 @@ const Description = React.forwardRef(functio }); }); -export const EmptyState = { Root, Icon: EmptyStateIcon, Label, Description, Actions }; +export { + Root as EmptyStateRoot, + EmptyStateIcon, + Label as EmptyStateLabel, + Description as EmptyStateDescription, + Actions as EmptyStateActions, +}; diff --git a/packages/mosaic/src/components/empty-state/index.ts b/packages/mosaic/src/components/empty-state/index.ts index 37ab78550f1..440e9331459 100644 --- a/packages/mosaic/src/components/empty-state/index.ts +++ b/packages/mosaic/src/components/empty-state/index.ts @@ -1,6 +1,4 @@ -'use client'; - -export { EmptyState } from './empty-state'; +export * as EmptyState from './empty-state.parts'; export type { EmptyStateActionsProps, EmptyStateDescriptionProps, diff --git a/packages/mosaic/src/components/entity/entity.browser.test.tsx b/packages/mosaic/src/components/entity/entity.browser.test.tsx index 9d6c39af54b..1dcdf1ca8c7 100644 --- a/packages/mosaic/src/components/entity/entity.browser.test.tsx +++ b/packages/mosaic/src/components/entity/entity.browser.test.tsx @@ -2,7 +2,7 @@ import { cleanup, render, screen } from '@testing-library/react'; import { afterEach, describe, expect, it } from 'vitest'; import { Badge } from '../badge'; -import { Entity } from './entity'; +import { Entity } from '.'; afterEach(() => cleanup()); diff --git a/packages/mosaic/src/components/entity/entity.parts.ts b/packages/mosaic/src/components/entity/entity.parts.ts new file mode 100644 index 00000000000..3c0790d989f --- /dev/null +++ b/packages/mosaic/src/components/entity/entity.parts.ts @@ -0,0 +1,7 @@ +export { + EntityRoot as Root, + EntityMedia as Media, + EntityContent as Content, + EntityLabel as Label, + EntityDescription as Description, +} from './entity'; diff --git a/packages/mosaic/src/components/entity/entity.test.tsx b/packages/mosaic/src/components/entity/entity.test.tsx index 00ba3e87f6f..271a66e39c7 100644 --- a/packages/mosaic/src/components/entity/entity.test.tsx +++ b/packages/mosaic/src/components/entity/entity.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { describe, expect, it } from 'vitest'; -import { Entity } from './entity'; +import { Entity } from '.'; const overrides = stylex.create({ root: { marginTop: '8px' }, diff --git a/packages/mosaic/src/components/entity/entity.tsx b/packages/mosaic/src/components/entity/entity.tsx index 0f3056ff5e6..eca4eccb882 100644 --- a/packages/mosaic/src/components/entity/entity.tsx +++ b/packages/mosaic/src/components/entity/entity.tsx @@ -1,3 +1,5 @@ +'use client'; + import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -84,10 +86,10 @@ const Description = React.forwardRef }); }); -export const Entity = { - Root, - Media, - Content, - Label, - Description, +export { + Root as EntityRoot, + Media as EntityMedia, + Content as EntityContent, + Label as EntityLabel, + Description as EntityDescription, }; diff --git a/packages/mosaic/src/components/entity/index.ts b/packages/mosaic/src/components/entity/index.ts index 8740fe3de49..6b816f9d4a9 100644 --- a/packages/mosaic/src/components/entity/index.ts +++ b/packages/mosaic/src/components/entity/index.ts @@ -1,3 +1 @@ -'use client'; - -export { Entity } from './entity'; +export * as Entity from './entity.parts'; diff --git a/packages/mosaic/src/components/field/__tests__/field.ssr.test.tsx b/packages/mosaic/src/components/field/__tests__/field.ssr.test.tsx index 4709b70e55a..b2fdaf47be3 100644 --- a/packages/mosaic/src/components/field/__tests__/field.ssr.test.tsx +++ b/packages/mosaic/src/components/field/__tests__/field.ssr.test.tsx @@ -5,7 +5,7 @@ import { renderToString } from 'react-dom/server'; import { describe, expect, it } from 'vitest'; import { Input } from '../../input'; -import { Field } from '../field'; +import { Field } from '..'; describe('Mosaic Field SSR', () => { it('emits render-time relationships and defers registered relationships until hydration', () => { diff --git a/packages/mosaic/src/components/field/__tests__/field.test.tsx b/packages/mosaic/src/components/field/__tests__/field.test.tsx index 23660d012c5..2d3a05a9e7b 100644 --- a/packages/mosaic/src/components/field/__tests__/field.test.tsx +++ b/packages/mosaic/src/components/field/__tests__/field.test.tsx @@ -11,7 +11,7 @@ import { Checkbox } from '../../checkbox'; import type { FieldFeedback } from '../../form'; import { Input } from '../../input'; import { Select } from '../../select'; -import { Field } from '../field'; +import { Field } from '..'; const overrides = stylex.create({ root: { display: 'grid' }, diff --git a/packages/mosaic/src/components/field/field.parts.ts b/packages/mosaic/src/components/field/field.parts.ts new file mode 100644 index 00000000000..799a14fd0fc --- /dev/null +++ b/packages/mosaic/src/components/field/field.parts.ts @@ -0,0 +1,11 @@ +export { + FieldRoot as Root, + FieldLabel as Label, + FieldContent as Content, + FieldDescription as Description, + FieldMessage as Message, + FieldFeedback as Feedback, + FieldError as Error, + FieldSuccess as Success, + FieldHint as Hint, +} from './field'; diff --git a/packages/mosaic/src/components/field/field.tsx b/packages/mosaic/src/components/field/field.tsx index 0703224c391..6e5cb5e7f10 100644 --- a/packages/mosaic/src/components/field/field.tsx +++ b/packages/mosaic/src/components/field/field.tsx @@ -1,3 +1,5 @@ +'use client'; + import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -323,15 +325,14 @@ const Feedback = React.forwardRef(function M ); }); -/** Styled parts for composing an automatically associated single-control field. */ -export const Field = { - Root, - Label, - Content, - Description, - Message, - Feedback, - Error: FieldError, - Success: FieldSuccess, - Hint: FieldHint, +export { + Root as FieldRoot, + Label as FieldLabel, + Content as FieldContent, + Description as FieldDescription, + Message as FieldMessage, + Feedback as FieldFeedback, + FieldError, + FieldSuccess, + FieldHint, }; diff --git a/packages/mosaic/src/components/field/index.ts b/packages/mosaic/src/components/field/index.ts index 9daf83fd93f..c707b84205c 100644 --- a/packages/mosaic/src/components/field/index.ts +++ b/packages/mosaic/src/components/field/index.ts @@ -1,4 +1,5 @@ -export { Field } from './field'; +/** Styled parts for composing an automatically associated single-control field. */ +export * as Field from './field.parts'; export type { FieldDescriptionProps, FieldErrorProps, diff --git a/packages/mosaic/src/components/flow/flow.parts.ts b/packages/mosaic/src/components/flow/flow.parts.ts new file mode 100644 index 00000000000..9908198d6d8 --- /dev/null +++ b/packages/mosaic/src/components/flow/flow.parts.ts @@ -0,0 +1 @@ +export { FlowRoot as Root, FlowStep as Step } from './flow'; diff --git a/packages/mosaic/src/components/flow/flow.test.tsx b/packages/mosaic/src/components/flow/flow.test.tsx index c198368d494..7f480d3ce2d 100644 --- a/packages/mosaic/src/components/flow/flow.test.tsx +++ b/packages/mosaic/src/components/flow/flow.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import { describe, expect, it } from 'vitest'; -import { Flow } from './flow'; +import { Flow } from '.'; describe('Mosaic Flow', () => { it('renders only the controlled step', () => { diff --git a/packages/mosaic/src/components/flow/flow.tsx b/packages/mosaic/src/components/flow/flow.tsx index d13c6ea3a5f..63defc38be1 100644 --- a/packages/mosaic/src/components/flow/flow.tsx +++ b/packages/mosaic/src/components/flow/flow.tsx @@ -1,3 +1,5 @@ +'use client'; + import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -66,7 +68,6 @@ const Step = React.forwardRef(function FlowStep( ); }); -/** A controlled set of screens driven by opaque caller state. */ -export const Flow = { Root, Step }; +export { Root as FlowRoot, Step as FlowStep }; export type { FlowDirection }; diff --git a/packages/mosaic/src/components/flow/index.ts b/packages/mosaic/src/components/flow/index.ts index fb9a336d9c0..c0e1ec2ff4a 100644 --- a/packages/mosaic/src/components/flow/index.ts +++ b/packages/mosaic/src/components/flow/index.ts @@ -1,3 +1,4 @@ export { useFlowAutoFocus } from '../../primitives/flow'; -export { Flow } from './flow'; +/** A controlled set of screens driven by opaque caller state. */ +export * as Flow from './flow.parts'; export type { FlowDirection, FlowRootProps, FlowStepProps } from './flow'; diff --git a/packages/mosaic/src/components/input-group/index.ts b/packages/mosaic/src/components/input-group/index.ts index 1fc3f2d8f41..66fb28c3c30 100644 --- a/packages/mosaic/src/components/input-group/index.ts +++ b/packages/mosaic/src/components/input-group/index.ts @@ -1,2 +1,2 @@ -export { InputGroup } from './input-group'; +export * as InputGroup from './input-group.parts'; export type { InputGroupAddonProps, InputGroupRootProps } from './input-group'; diff --git a/packages/mosaic/src/components/input-group/input-group.parts.ts b/packages/mosaic/src/components/input-group/input-group.parts.ts new file mode 100644 index 00000000000..23cb8f0af9d --- /dev/null +++ b/packages/mosaic/src/components/input-group/input-group.parts.ts @@ -0,0 +1,6 @@ +export { + InputGroupRoot as Root, + InputGroupInput as Input, + InputGroupStart as Start, + InputGroupEnd as End, +} from './input-group'; diff --git a/packages/mosaic/src/components/input-group/input-group.test.tsx b/packages/mosaic/src/components/input-group/input-group.test.tsx index 733ff691d92..4259ccb8ac6 100644 --- a/packages/mosaic/src/components/input-group/input-group.test.tsx +++ b/packages/mosaic/src/components/input-group/input-group.test.tsx @@ -6,7 +6,7 @@ import { describe, expect, it, vi } from 'vitest'; import { Button } from '../button'; import { Field } from '../field'; import { Input } from '../input'; -import { InputGroup } from './input-group'; +import { InputGroup } from '.'; describe('Mosaic InputGroup', () => { it('exposes slots rather than a separate action component', () => { diff --git a/packages/mosaic/src/components/input-group/input-group.tsx b/packages/mosaic/src/components/input-group/input-group.tsx index 2399388a6e7..1c0bd59404f 100644 --- a/packages/mosaic/src/components/input-group/input-group.tsx +++ b/packages/mosaic/src/components/input-group/input-group.tsx @@ -128,4 +128,4 @@ const End = React.forwardRef(function Mos return useAddon('end', props, ref); }); -export const InputGroup = { Root, Input, Start, End }; +export { Root as InputGroupRoot, Input as InputGroupInput, Start as InputGroupStart, End as InputGroupEnd }; diff --git a/packages/mosaic/src/components/item/index.ts b/packages/mosaic/src/components/item/index.ts index a0a8cfa20eb..e4560ed0d38 100644 --- a/packages/mosaic/src/components/item/index.ts +++ b/packages/mosaic/src/components/item/index.ts @@ -1,4 +1,8 @@ -'use client'; - -export { Item } from './item'; +/** + * Mosaic `Item` — a row for lists of accounts, organizations, and settings. + * Composed via dot syntax: `Item.Root`, `Item.Media`, `Item.Content`, + * `Item.Label`, `Item.Description`, `Item.Actions`, `Item.Group`. Every part + * takes a `render` prop and forwards a ref. + */ +export * as Item from './item.parts'; export type { ItemProps } from './item'; diff --git a/packages/mosaic/src/components/item/item.parts.ts b/packages/mosaic/src/components/item/item.parts.ts new file mode 100644 index 00000000000..c35bc5885d2 --- /dev/null +++ b/packages/mosaic/src/components/item/item.parts.ts @@ -0,0 +1,9 @@ +export { + ItemRoot as Root, + ItemMedia as Media, + ItemContent as Content, + ItemLabel as Label, + ItemDescription as Description, + ItemActions as Actions, + ItemGroup as Group, +} from './item'; diff --git a/packages/mosaic/src/components/item/item.test.tsx b/packages/mosaic/src/components/item/item.test.tsx index 5dcf07fcbd1..27cf8ac815b 100644 --- a/packages/mosaic/src/components/item/item.test.tsx +++ b/packages/mosaic/src/components/item/item.test.tsx @@ -3,7 +3,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { describe, expect, it } from 'vitest'; -import { Item } from './item'; +import { Item } from '.'; const overrides = stylex.create({ root: { marginTop: '8px' }, diff --git a/packages/mosaic/src/components/item/item.tsx b/packages/mosaic/src/components/item/item.tsx index a694c201063..7a9ee1d3893 100644 --- a/packages/mosaic/src/components/item/item.tsx +++ b/packages/mosaic/src/components/item/item.tsx @@ -1,3 +1,5 @@ +'use client'; + import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -166,18 +168,12 @@ const Group = React.forwardRef>(func }); }); -/** - * Mosaic `Item` — a row for lists of accounts, organizations, and settings. - * Composed via dot syntax: `Item.Root`, `Item.Media`, `Item.Content`, - * `Item.Label`, `Item.Description`, `Item.Actions`, `Item.Group`. Every part - * takes a `render` prop and forwards a ref. - */ -export const Item = { - Root, - Media, - Content, - Label, - Description, - Actions, - Group, +export { + Root as ItemRoot, + Media as ItemMedia, + Content as ItemContent, + Label as ItemLabel, + Description as ItemDescription, + Actions as ItemActions, + Group as ItemGroup, }; diff --git a/packages/mosaic/src/components/menu/index.ts b/packages/mosaic/src/components/menu/index.ts index f2855739fdd..07934197b9a 100644 --- a/packages/mosaic/src/components/menu/index.ts +++ b/packages/mosaic/src/components/menu/index.ts @@ -1,4 +1,5 @@ -export { Menu, MenuItem, MenuLabel, MenuMedia, MenuPopup, MenuSeparator, MenuTrigger } from './menu'; +export * as Menu from './menu.parts'; +export { MenuItem, MenuLabel, MenuMedia, MenuPopup, MenuSeparator, MenuTrigger } from './menu'; export type { MenuItemProps, MenuLabelProps, diff --git a/packages/mosaic/src/components/menu/menu.parts.ts b/packages/mosaic/src/components/menu/menu.parts.ts new file mode 100644 index 00000000000..01246afffbd --- /dev/null +++ b/packages/mosaic/src/components/menu/menu.parts.ts @@ -0,0 +1,10 @@ +export { + MenuRoot as Root, + MenuTrigger as Trigger, + MenuPopup as Popup, + MenuItem as Item, + MenuMedia as Media, + MenuLabel as Label, + MenuIndicator as Indicator, + MenuSeparator as Separator, +} from './menu'; diff --git a/packages/mosaic/src/components/menu/menu.test.tsx b/packages/mosaic/src/components/menu/menu.test.tsx index 7046923cea4..96417089c14 100644 --- a/packages/mosaic/src/components/menu/menu.test.tsx +++ b/packages/mosaic/src/components/menu/menu.test.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { describe, expect, it, vi } from 'vitest'; import { scrollAreaRoot, scrollAreaViewport } from '../scroll-area'; -import { Menu } from './menu'; +import { Menu } from '.'; const scrollClasses = stylex.props(...scrollAreaViewport()).className?.split(' ') ?? []; // The popup shares the root's atoms (both are `min-height: 0` flex columns), so only what the diff --git a/packages/mosaic/src/components/menu/menu.tsx b/packages/mosaic/src/components/menu/menu.tsx index d6d0946c9b7..1978afd0e3b 100644 --- a/packages/mosaic/src/components/menu/menu.tsx +++ b/packages/mosaic/src/components/menu/menu.tsx @@ -1,3 +1,5 @@ +'use client'; + import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -239,15 +241,7 @@ export const MenuSeparator = React.forwardRef + * Account + * + * … + * + * + * ``` + */ +export * as Panel from './panel.parts'; +export { usePanelTitle } from './panel'; export type { PanelRootProps, PanelSectionsProps, PanelTitleProps } from './panel'; diff --git a/packages/mosaic/src/components/panel/panel.parts.ts b/packages/mosaic/src/components/panel/panel.parts.ts new file mode 100644 index 00000000000..a167c8d3a7c --- /dev/null +++ b/packages/mosaic/src/components/panel/panel.parts.ts @@ -0,0 +1 @@ +export { PanelRoot as Root, PanelTitle as Title, PanelSections as Sections } from './panel'; diff --git a/packages/mosaic/src/components/panel/panel.test.tsx b/packages/mosaic/src/components/panel/panel.test.tsx index aeb80129b88..6bced1a73ae 100644 --- a/packages/mosaic/src/components/panel/panel.test.tsx +++ b/packages/mosaic/src/components/panel/panel.test.tsx @@ -6,7 +6,7 @@ import { MosaicProvider } from '../../mosaic-provider'; import { HeadingLevelProvider } from '../heading'; import { Profile } from '../profile'; import { Section } from '../section'; -import { Panel } from './panel'; +import { Panel } from '.'; function Account() { return ( diff --git a/packages/mosaic/src/components/panel/panel.tsx b/packages/mosaic/src/components/panel/panel.tsx index 4c17fa8b7c1..97f7b13fa61 100644 --- a/packages/mosaic/src/components/panel/panel.tsx +++ b/packages/mosaic/src/components/panel/panel.tsx @@ -1,3 +1,5 @@ +'use client'; + import { useMergeRefs } from '@floating-ui/react'; import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -80,16 +82,4 @@ const Sections = React.forwardRef(function P return {element}; }); -/** - * A page of content. `Panel.Sections` sit one heading level below `Panel.Title`. - * - * ```tsx - * - * Account - * - * … - * - * - * ``` - */ -export const Panel = { Root, Title, Sections }; +export { Root as PanelRoot, Title as PanelTitle, Sections as PanelSections }; diff --git a/packages/mosaic/src/components/popover/index.ts b/packages/mosaic/src/components/popover/index.ts index 8ac0cb8c76b..9f9a1f79cd6 100644 --- a/packages/mosaic/src/components/popover/index.ts +++ b/packages/mosaic/src/components/popover/index.ts @@ -1,4 +1,5 @@ -export { Popover } from './popover'; +/** A floating box anchored to a trigger. */ +export * as Popover from './popover.parts'; export type { PopoverCloseProps, PopoverDescriptionProps, diff --git a/packages/mosaic/src/components/popover/popover.parts.ts b/packages/mosaic/src/components/popover/popover.parts.ts new file mode 100644 index 00000000000..a22fb8ab6d5 --- /dev/null +++ b/packages/mosaic/src/components/popover/popover.parts.ts @@ -0,0 +1,8 @@ +export { + PopoverRoot as Root, + PopoverTrigger as Trigger, + PopoverPopup as Popup, + PopoverTitle as Title, + PopoverDescription as Description, + PopoverClose as Close, +} from './popover'; diff --git a/packages/mosaic/src/components/popover/popover.test.tsx b/packages/mosaic/src/components/popover/popover.test.tsx index 7a42e87f753..cc15f6d5f81 100644 --- a/packages/mosaic/src/components/popover/popover.test.tsx +++ b/packages/mosaic/src/components/popover/popover.test.tsx @@ -7,7 +7,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { Button } from '../button'; import { ButtonContext } from '../button/button.context'; import { Card } from '../card'; -import { Popover } from './popover'; +import { Popover } from '.'; afterEach(() => cleanup()); diff --git a/packages/mosaic/src/components/popover/popover.tsx b/packages/mosaic/src/components/popover/popover.tsx index d79f6e65a0e..3797f5116b2 100644 --- a/packages/mosaic/src/components/popover/popover.tsx +++ b/packages/mosaic/src/components/popover/popover.tsx @@ -1,3 +1,5 @@ +'use client'; + import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -114,12 +116,12 @@ const Popup = React.forwardRef(function Popov ); }); -/** A floating box anchored to a trigger. */ -export const Popover = { - Root: Primitive.Root, - Trigger, - Popup, - Title, - Description, - Close, +const PopoverRoot = Primitive.Root; +export { + PopoverRoot, + Trigger as PopoverTrigger, + Popup as PopoverPopup, + Title as PopoverTitle, + Description as PopoverDescription, + Close as PopoverClose, }; diff --git a/packages/mosaic/src/components/profile/index.ts b/packages/mosaic/src/components/profile/index.ts index 9f9d3b6f073..3b4f29cb72d 100644 --- a/packages/mosaic/src/components/profile/index.ts +++ b/packages/mosaic/src/components/profile/index.ts @@ -1,4 +1,19 @@ -export { Profile } from './profile'; +/** + * A surface you navigate: a column of destinations beside the page each one opens. + * + * ```tsx + * + * User profile + * + * }>Account + * + * + * … + * + * + * ``` + */ +export * as Profile from './profile.parts'; export type { ProfileContentProps, ProfileElevation, diff --git a/packages/mosaic/src/components/profile/profile.parts.ts b/packages/mosaic/src/components/profile/profile.parts.ts new file mode 100644 index 00000000000..d65eaf69dcc --- /dev/null +++ b/packages/mosaic/src/components/profile/profile.parts.ts @@ -0,0 +1,9 @@ +export { + ProfileRoot as Root, + ProfileTitle as Title, + ProfileNav as Nav, + ProfileNavGroup as NavGroup, + ProfileNavItem as NavItem, + ProfileContent as Content, + ProfileContentPanel as ContentPanel, +} from './profile'; diff --git a/packages/mosaic/src/components/profile/profile.ssr.test.tsx b/packages/mosaic/src/components/profile/profile.ssr.test.tsx index 14ac0e6e101..9dd92b3073a 100644 --- a/packages/mosaic/src/components/profile/profile.ssr.test.tsx +++ b/packages/mosaic/src/components/profile/profile.ssr.test.tsx @@ -5,7 +5,7 @@ import { renderToString } from 'react-dom/server'; import { describe, expect, it } from 'vitest'; import { MosaicProvider } from '../../mosaic-provider'; -import { Profile } from './profile'; +import { Profile } from '.'; describe('Profile SSR', () => { it('names the page in the page title heading on the server', () => { diff --git a/packages/mosaic/src/components/profile/profile.test.tsx b/packages/mosaic/src/components/profile/profile.test.tsx index 0e36bedafef..0bd64d7d0a0 100644 --- a/packages/mosaic/src/components/profile/profile.test.tsx +++ b/packages/mosaic/src/components/profile/profile.test.tsx @@ -15,7 +15,7 @@ import { Icon } from '../icon'; import { Panel, usePanelTitle } from '../panel'; import { Section } from '../section'; import type { ProfileRootProps } from './profile'; -import { Profile } from './profile'; +import { Profile } from '.'; const pageTitles: Record = { account: 'Account', security: 'Security' }; diff --git a/packages/mosaic/src/components/profile/profile.tsx b/packages/mosaic/src/components/profile/profile.tsx index 2e81cb034fb..3fd76968262 100644 --- a/packages/mosaic/src/components/profile/profile.tsx +++ b/packages/mosaic/src/components/profile/profile.tsx @@ -1,3 +1,5 @@ +'use client'; + import { useSafeLayoutEffect } from '@clerk/shared/react'; import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -668,19 +670,12 @@ const ContentPanel = React.forwardRef( ); }); -/** - * A surface you navigate: a column of destinations beside the page each one opens. - * - * ```tsx - * - * User profile - * - * }>Account - * - * - * … - * - * - * ``` - */ -export const Profile = { Root, Title, Nav, NavGroup, NavItem, Content, ContentPanel }; +export { + Root as ProfileRoot, + Title as ProfileTitle, + Nav as ProfileNav, + NavGroup as ProfileNavGroup, + NavItem as ProfileNavItem, + Content as ProfileContent, + ContentPanel as ProfileContentPanel, +}; diff --git a/packages/mosaic/src/components/radio/index.ts b/packages/mosaic/src/components/radio/index.ts index 7d7a93d49cc..15d9f463e20 100644 --- a/packages/mosaic/src/components/radio/index.ts +++ b/packages/mosaic/src/components/radio/index.ts @@ -1,2 +1,2 @@ -export { Radio } from './radio'; +export * as Radio from './radio.parts'; export type { RadioCardProps, RadioGroupProps, RadioItemProps, RadioLayout, RadioSectionProps } from './radio'; diff --git a/packages/mosaic/src/components/radio/radio.parts.ts b/packages/mosaic/src/components/radio/radio.parts.ts new file mode 100644 index 00000000000..ce304a68bfb --- /dev/null +++ b/packages/mosaic/src/components/radio/radio.parts.ts @@ -0,0 +1,10 @@ +export { + RadioGroup as Group, + RadioItem as Item, + RadioSection as Section, + RadioCard as Card, + RadioMedia as Media, + RadioContent as Content, + RadioLabel as Label, + RadioDescription as Description, +} from './radio'; diff --git a/packages/mosaic/src/components/radio/radio.test-d.tsx b/packages/mosaic/src/components/radio/radio.test-d.tsx index 49e04a99021..152f1ee6a9a 100644 --- a/packages/mosaic/src/components/radio/radio.test-d.tsx +++ b/packages/mosaic/src/components/radio/radio.test-d.tsx @@ -1,6 +1,6 @@ import { describe, expectTypeOf, test } from 'vitest'; -import { Radio } from './radio'; +import { Radio } from '.'; type Access = 'all' | 'selected'; diff --git a/packages/mosaic/src/components/radio/radio.test.tsx b/packages/mosaic/src/components/radio/radio.test.tsx index 9017b7dd36b..157ed7ec4c7 100644 --- a/packages/mosaic/src/components/radio/radio.test.tsx +++ b/packages/mosaic/src/components/radio/radio.test.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { describe, expect, it, vi } from 'vitest'; import { Field } from '../field'; -import { Radio } from './radio'; +import { Radio } from '.'; const atoms = stylex.create({ spaced: { marginTop: '8px' }, diff --git a/packages/mosaic/src/components/radio/radio.tsx b/packages/mosaic/src/components/radio/radio.tsx index 1c30af39a46..d67e50e9439 100644 --- a/packages/mosaic/src/components/radio/radio.tsx +++ b/packages/mosaic/src/components/radio/radio.tsx @@ -1,3 +1,5 @@ +'use client'; + import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -262,13 +264,8 @@ const RadioCard = React.forwardRef(function Mo ); }); -export const Radio = { - Group: RadioGroup, - Item: RadioItem, - Section: RadioSection, - Card: RadioCard, - Media: Item.Media, - Content: Item.Content, - Label: Item.Label, - Description: Item.Description, -}; +const RadioMedia = Item.Media; +const RadioContent = Item.Content; +const RadioLabel = Item.Label; +const RadioDescription = Item.Description; +export { RadioGroup, RadioItem, RadioSection, RadioCard, RadioMedia, RadioContent, RadioLabel, RadioDescription }; diff --git a/packages/mosaic/src/components/section/index.ts b/packages/mosaic/src/components/section/index.ts index 540434b0ca1..6289fa8ec37 100644 --- a/packages/mosaic/src/components/section/index.ts +++ b/packages/mosaic/src/components/section/index.ts @@ -1,4 +1,9 @@ -export { Section } from './section'; +/** + * A compound component for a topic of settings. `Section.Root` stacks groups; each `Section.Group` + * is named by the `Section.Title` in its `Section.Header`, above a `Section.Body` card holding + * either `Section.Row`s (one setting each) or a `Section.Items` list of values. + */ +export * as Section from './section.parts'; export { sectionCompactStyles } from './section.styles'; export type { SectionActionsProps, diff --git a/packages/mosaic/src/components/section/section.parts.ts b/packages/mosaic/src/components/section/section.parts.ts new file mode 100644 index 00000000000..3350feb29ff --- /dev/null +++ b/packages/mosaic/src/components/section/section.parts.ts @@ -0,0 +1,17 @@ +export { + SectionRoot as Root, + SectionGroup as Group, + SectionHeader as Header, + SectionTitle as Title, + SectionBody as Body, + SectionRow as Row, + SectionItems as Items, + SectionItem as Item, + SectionMedia as Media, + SectionContent as Content, + SectionLabel as Label, + SectionDescription as Description, + SectionActions as Actions, + SectionNote as Note, + SectionError as Error, +} from './section'; diff --git a/packages/mosaic/src/components/section/section.test.tsx b/packages/mosaic/src/components/section/section.test.tsx index b862b4a3271..29f56d4ebc4 100644 --- a/packages/mosaic/src/components/section/section.test.tsx +++ b/packages/mosaic/src/components/section/section.test.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { describe, expect, it } from 'vitest'; import { HeadingLevelProvider } from '../heading'; -import { Section } from './section'; +import { Section } from '.'; const overrides = stylex.create({ root: { containerType: 'inline-size' }, diff --git a/packages/mosaic/src/components/section/section.tsx b/packages/mosaic/src/components/section/section.tsx index e3435143c62..d0e7d3768c4 100644 --- a/packages/mosaic/src/components/section/section.tsx +++ b/packages/mosaic/src/components/section/section.tsx @@ -1,3 +1,5 @@ +'use client'; + import { useSafeLayoutEffect } from '@clerk/shared/react'; import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -347,25 +349,20 @@ const SectionError = React.forwardRef(f }); }); -/** - * A compound component for a topic of settings. `Section.Root` stacks groups; each `Section.Group` - * is named by the `Section.Title` in its `Section.Header`, above a `Section.Body` card holding - * either `Section.Row`s (one setting each) or a `Section.Items` list of values. - */ -export const Section = { - Root, - Group, - Header, - Title, - Body, - Row, - Items, - Item, - Media, - Content, - Label, - Description, - Actions, - Note, - Error: SectionError, +export { + Root as SectionRoot, + Group as SectionGroup, + Header as SectionHeader, + Title as SectionTitle, + Body as SectionBody, + Row as SectionRow, + Items as SectionItems, + Item as SectionItem, + Media as SectionMedia, + Content as SectionContent, + Label as SectionLabel, + Description as SectionDescription, + Actions as SectionActions, + Note as SectionNote, + SectionError, }; diff --git a/packages/mosaic/src/components/segmented-control/index.ts b/packages/mosaic/src/components/segmented-control/index.ts index 19245cca3e7..3664427d9d5 100644 --- a/packages/mosaic/src/components/segmented-control/index.ts +++ b/packages/mosaic/src/components/segmented-control/index.ts @@ -1,2 +1,2 @@ -export { SegmentedControl } from './segmented-control'; +export * as SegmentedControl from './segmented-control.parts'; export type { SegmentedControlItemProps, SegmentedControlRootProps } from './segmented-control'; diff --git a/packages/mosaic/src/components/segmented-control/segmented-control.parts.ts b/packages/mosaic/src/components/segmented-control/segmented-control.parts.ts new file mode 100644 index 00000000000..3a1388e1b5c --- /dev/null +++ b/packages/mosaic/src/components/segmented-control/segmented-control.parts.ts @@ -0,0 +1 @@ +export { SegmentedControlRoot as Root, SegmentedControlItem as Item } from './segmented-control'; diff --git a/packages/mosaic/src/components/segmented-control/segmented-control.test-d.tsx b/packages/mosaic/src/components/segmented-control/segmented-control.test-d.tsx index a06c6d33696..1b8f510ae09 100644 --- a/packages/mosaic/src/components/segmented-control/segmented-control.test-d.tsx +++ b/packages/mosaic/src/components/segmented-control/segmented-control.test-d.tsx @@ -1,6 +1,6 @@ import { describe, expectTypeOf, test } from 'vitest'; -import { SegmentedControl } from './segmented-control'; +import { SegmentedControl } from '.'; type Access = 'all' | 'selected'; diff --git a/packages/mosaic/src/components/segmented-control/segmented-control.test.tsx b/packages/mosaic/src/components/segmented-control/segmented-control.test.tsx index b16df7a9750..366aafa5258 100644 --- a/packages/mosaic/src/components/segmented-control/segmented-control.test.tsx +++ b/packages/mosaic/src/components/segmented-control/segmented-control.test.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { describe, expect, it, vi } from 'vitest'; import { DirectionProvider } from '../../primitives/direction-provider'; -import { SegmentedControl } from './segmented-control'; +import { SegmentedControl } from '.'; function BillingPeriod(props: Partial>) { return ( diff --git a/packages/mosaic/src/components/segmented-control/segmented-control.tsx b/packages/mosaic/src/components/segmented-control/segmented-control.tsx index 5e0db2801c7..7017435922a 100644 --- a/packages/mosaic/src/components/segmented-control/segmented-control.tsx +++ b/packages/mosaic/src/components/segmented-control/segmented-control.tsx @@ -1,3 +1,5 @@ +'use client'; + import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -61,7 +63,4 @@ const Item = React.forwardRef(func ); }); -export const SegmentedControl = { - Root, - Item, -}; +export { Root as SegmentedControlRoot, Item as SegmentedControlItem }; diff --git a/packages/mosaic/src/components/select/index.ts b/packages/mosaic/src/components/select/index.ts index 3994f585f06..1b38ac74676 100644 --- a/packages/mosaic/src/components/select/index.ts +++ b/packages/mosaic/src/components/select/index.ts @@ -1,5 +1,5 @@ +export * as Select from './select.parts'; export { - Select, SelectContent, SelectDescription, SelectLabel, diff --git a/packages/mosaic/src/components/select/select.parts.ts b/packages/mosaic/src/components/select/select.parts.ts new file mode 100644 index 00000000000..acc1d4143c8 --- /dev/null +++ b/packages/mosaic/src/components/select/select.parts.ts @@ -0,0 +1,10 @@ +export { + SelectRoot as Root, + SelectTrigger as Trigger, + SelectPopup as Popup, + SelectOption as Option, + SelectMedia as Media, + SelectContent as Content, + SelectLabel as Label, + SelectDescription as Description, +} from './select'; diff --git a/packages/mosaic/src/components/select/select.test-d.tsx b/packages/mosaic/src/components/select/select.test-d.tsx index 2d45ce0b30c..d921d4d3472 100644 --- a/packages/mosaic/src/components/select/select.test-d.tsx +++ b/packages/mosaic/src/components/select/select.test-d.tsx @@ -1,6 +1,6 @@ import { describe, expectTypeOf, test } from 'vitest'; -import { Select } from './select'; +import { Select } from '.'; type Expiration = '1d' | '7d'; diff --git a/packages/mosaic/src/components/select/select.test.tsx b/packages/mosaic/src/components/select/select.test.tsx index 5e52777d0dd..e4af1a22786 100644 --- a/packages/mosaic/src/components/select/select.test.tsx +++ b/packages/mosaic/src/components/select/select.test.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { describe, expect, it, vi } from 'vitest'; import { Field } from '../field'; -import { Select } from './select'; +import { Select } from '.'; const roles = [ { value: 'all', label: 'All roles' }, diff --git a/packages/mosaic/src/components/select/select.tsx b/packages/mosaic/src/components/select/select.tsx index a302c047eac..f432f6acf7f 100644 --- a/packages/mosaic/src/components/select/select.tsx +++ b/packages/mosaic/src/components/select/select.tsx @@ -1,3 +1,5 @@ +'use client'; + import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -381,14 +383,3 @@ export const SelectDescription = React.forwardRef ); }); -export const Table = { - Root, - Toolbar, - Search, - Header, - Column, - Body, - Row, - Cell, - SelectionColumn, - SelectionCell, - ExpandButton, +export { + Root as TableRoot, + Toolbar as TableToolbar, + Search as TableSearch, + Header as TableHeader, + Column as TableColumn, + Body as TableBody, + Row as TableRow, + Cell as TableCell, + SelectionColumn as TableSelectionColumn, + SelectionCell as TableSelectionCell, + ExpandButton as TableExpandButton, }; diff --git a/packages/mosaic/src/components/tabs/index.ts b/packages/mosaic/src/components/tabs/index.ts index 9cf08932446..86941410a2c 100644 --- a/packages/mosaic/src/components/tabs/index.ts +++ b/packages/mosaic/src/components/tabs/index.ts @@ -1,4 +1,4 @@ -export { Tabs } from './tabs'; +export * as Tabs from './tabs.parts'; export type { TabsListProps, TabsPanelProps, diff --git a/packages/mosaic/src/components/tabs/tabs.parts.ts b/packages/mosaic/src/components/tabs/tabs.parts.ts new file mode 100644 index 00000000000..0955d6e4d7b --- /dev/null +++ b/packages/mosaic/src/components/tabs/tabs.parts.ts @@ -0,0 +1,8 @@ +export { + TabsRoot as Root, + TabsList as List, + TabsTab as Tab, + TabsTrigger as Trigger, + TabsPanels as Panels, + TabsPanel as Panel, +} from './tabs'; diff --git a/packages/mosaic/src/components/tabs/tabs.test.tsx b/packages/mosaic/src/components/tabs/tabs.test.tsx index 8d00af583a6..cc4090462ca 100644 --- a/packages/mosaic/src/components/tabs/tabs.test.tsx +++ b/packages/mosaic/src/components/tabs/tabs.test.tsx @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { describe, expect, it } from 'vitest'; -import { Tabs } from './tabs'; +import { Tabs } from '.'; import { styles } from './tabs.styles'; const atoms = stylex.create({ diff --git a/packages/mosaic/src/components/tabs/tabs.tsx b/packages/mosaic/src/components/tabs/tabs.tsx index c1380c2671f..2a5e0f20585 100644 --- a/packages/mosaic/src/components/tabs/tabs.tsx +++ b/packages/mosaic/src/components/tabs/tabs.tsx @@ -1,3 +1,5 @@ +'use client'; + import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -112,11 +114,11 @@ const Panels = React.forwardRef(function Mosaic }); }); -export const Tabs = { - Root, - List, - Tab, - Trigger, - Panels, - Panel, +export { + Root as TabsRoot, + List as TabsList, + Tab as TabsTab, + Trigger as TabsTrigger, + Panels as TabsPanels, + Panel as TabsPanel, }; diff --git a/packages/mosaic/src/components/tooltip/index.ts b/packages/mosaic/src/components/tooltip/index.ts index bea23eca103..a56818220ce 100644 --- a/packages/mosaic/src/components/tooltip/index.ts +++ b/packages/mosaic/src/components/tooltip/index.ts @@ -1,2 +1,2 @@ -export { Tooltip } from './tooltip'; +export * as Tooltip from './tooltip.parts'; export type { TooltipPopupProps, TooltipRootProps, TooltipTriggerProps } from './tooltip'; diff --git a/packages/mosaic/src/components/tooltip/tooltip.parts.ts b/packages/mosaic/src/components/tooltip/tooltip.parts.ts new file mode 100644 index 00000000000..5a09be7d3f4 --- /dev/null +++ b/packages/mosaic/src/components/tooltip/tooltip.parts.ts @@ -0,0 +1,6 @@ +export { + TooltipRoot as Root, + TooltipGroup as Group, + TooltipTrigger as Trigger, + TooltipPopup as Popup, +} from './tooltip'; diff --git a/packages/mosaic/src/components/tooltip/tooltip.test.tsx b/packages/mosaic/src/components/tooltip/tooltip.test.tsx index 88d588d0bbf..239ae3f4699 100644 --- a/packages/mosaic/src/components/tooltip/tooltip.test.tsx +++ b/packages/mosaic/src/components/tooltip/tooltip.test.tsx @@ -4,7 +4,7 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { afterEach, describe, expect, it } from 'vitest'; -import { Tooltip } from './tooltip'; +import { Tooltip } from '.'; afterEach(() => cleanup()); diff --git a/packages/mosaic/src/components/tooltip/tooltip.tsx b/packages/mosaic/src/components/tooltip/tooltip.tsx index cdc41232a48..dffffb41e3e 100644 --- a/packages/mosaic/src/components/tooltip/tooltip.tsx +++ b/packages/mosaic/src/components/tooltip/tooltip.tsx @@ -1,3 +1,5 @@ +'use client'; + import * as stylex from '@stylexjs/stylex'; import React from 'react'; @@ -42,9 +44,6 @@ const Popup = React.forwardRef(function Toolt ); }); -export const Tooltip = { - Root: Primitive.Root, - Group: Primitive.Group, - Trigger, - Popup, -}; +const TooltipRoot = Primitive.Root; +const TooltipGroup = Primitive.Group; +export { TooltipRoot, TooltipGroup, Trigger as TooltipTrigger, Popup as TooltipPopup }; diff --git a/packages/mosaic/test/rsc/app/layout.jsx b/packages/mosaic/test/rsc/app/layout.jsx new file mode 100644 index 00000000000..9af189d31d5 --- /dev/null +++ b/packages/mosaic/test/rsc/app/layout.jsx @@ -0,0 +1,7 @@ +export default function RootLayout({ children }) { + return ( + + {children} + + ); +} diff --git a/packages/mosaic/test/rsc/app/unrendered.jsx b/packages/mosaic/test/rsc/app/unrendered.jsx new file mode 100644 index 00000000000..f69bb9d72fe --- /dev/null +++ b/packages/mosaic/test/rsc/app/unrendered.jsx @@ -0,0 +1,5 @@ +'use client'; + +export function Unrendered() { + return null; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc4fae29ab5..25c0562fa62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1033,6 +1033,9 @@ importers: msw: specifier: 2.15.0 version: 2.15.0(@types/node@22.20.1)(typescript@7.0.2) + next: + specifier: ^16.4.3 + version: 16.4.5(@babel/core@7.29.7(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.62.1)(@types/node@22.20.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) playwright: specifier: 1.62.1 version: 1.62.1 @@ -1065,7 +1068,7 @@ importers: version: 2.13.6(@types/node@22.20.1)(typescript@6.0.3) next: specifier: '>=15.5.18' - version: 16.4.5(@babel/core@7.29.7(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.62.1)(@types/node@22.20.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 16.4.6(@babel/core@7.29.7(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.62.1)(@types/node@22.20.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: 18.3.1 version: 18.3.1 @@ -4165,6 +4168,9 @@ packages: '@next/env@16.4.5': resolution: {integrity: sha512-JwSPwpdkZL5VMl2jA/q+JI8MZdweAL3xmw5/tb9y/TLtmS0YXZnmYDzSihwk8xTj1OL+WM53gTqaj96yuLp6Xw==} + '@next/env@16.4.6': + resolution: {integrity: sha512-SunBjJ6t+oBtecS3dTjdVuMhUogP5wD/vrGL5EVuQYM1iS4O5ilL51HkiJ3sPjMzRHiCChvr4dbaSJbR7F3lxA==} + '@next/mdx@16.4.3': resolution: {integrity: sha512-N86fgGEVCoB0ULnWlmRZcaZnZD5sLmBkZt4CAPxy0b04qpwimlCdhA6MnwdnJBqmByl2OEKHpQt/GpJRz3UUwQ==} peerDependencies: @@ -4188,6 +4194,12 @@ packages: cpu: [arm64] os: [darwin] + '@next/swc-darwin-arm64@16.4.6': + resolution: {integrity: sha512-kJ4VVZkley17xKaJx72QN2IBqjrj7zenCQZaUF0l9ShS/1Mt2kW4+Z5KGG6vgr3L/J562xTOizVyM1iA/u5nlg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + '@next/swc-darwin-x64@15.5.24': resolution: {integrity: sha512-9HrQajBMmGcrrrvDfRimiCrbAPh3E6uHJmwBovYr6Yrmi9p9PZqI876BrXX280wICh3o2XwUlp4blkB0NNBqFg==} engines: {node: '>= 10'} @@ -4200,6 +4212,12 @@ packages: cpu: [x64] os: [darwin] + '@next/swc-darwin-x64@16.4.6': + resolution: {integrity: sha512-eIPGZIqC+8JKaeZmvnCANJQF4xWkkHqSnAAajtFxgwDMAOnCgxNvP52SeWZ41PyVwoe+Le/NQuZSaQ8j7YshSA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + '@next/swc-linux-arm64-gnu@15.5.24': resolution: {integrity: sha512-rl9LSfE75si0WT3cDgdUC1XYCKS+TgxC+/IjitmeycrAG18X/plIP1/vy8dd/HPycYcIvE688PD7FuvEAiEAew==} engines: {node: '>= 10'} @@ -4214,6 +4232,13 @@ packages: os: [linux] libc: [glibc] + '@next/swc-linux-arm64-gnu@16.4.6': + resolution: {integrity: sha512-9nI6O8vvczH2D9ZNdMGqb/3NuT15T9ksOZnAmx9/gjF2cOi74+xxyf1Hk96uIMfm14c9y6wHeBq6SH8g6udvcA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@next/swc-linux-arm64-musl@15.5.24': resolution: {integrity: sha512-TlNAnpsjxSF3aAUtqnfmtXXf8m9sIDBlmF3c7bTAlnshUYu2U0OxN2uf5d0gcFwqHVEdivJNBcCaqNOwPGNimw==} engines: {node: '>= 10'} @@ -4228,6 +4253,13 @@ packages: os: [linux] libc: [musl] + '@next/swc-linux-arm64-musl@16.4.6': + resolution: {integrity: sha512-7/qPhTxDp2ScluOJOhLuhWmlA17o1yZ0SIgpISBCpBxXgtnK4+KGywBpFkfLPiU1rnSM1faFGzYkH6i7JdTrqA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + '@next/swc-linux-x64-gnu@15.5.24': resolution: {integrity: sha512-7dwtlhr0SLndqTG1z9ncRkbJswDZiKWlxzFyXDvJ2RDZRDRHp8zyMJ4D9UH/FgnQeXxxB6gZy2pMcIUoNKQ4pA==} engines: {node: '>= 10'} @@ -4242,6 +4274,13 @@ packages: os: [linux] libc: [glibc] + '@next/swc-linux-x64-gnu@16.4.6': + resolution: {integrity: sha512-oOpbNa8XOW1qCGd9VvzC4fbkkRxEls7/WI6tz+YHC0xRA7KpV1U7DrAnCPjzVyOk/karzWEepCOcTIDmwK8FwQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + '@next/swc-linux-x64-musl@15.5.24': resolution: {integrity: sha512-kGZxM+WhkYs0276lFrMkj7PRtXT3Btp6cwvfSO/cCVLxJJttB5Ccnl2niaCgUja8HgSbEVnMHpg3FJWoOJ9e/g==} engines: {node: '>= 10'} @@ -4256,6 +4295,13 @@ packages: os: [linux] libc: [musl] + '@next/swc-linux-x64-musl@16.4.6': + resolution: {integrity: sha512-aM8CZ+OR2EwXaAYw7KGkW2GDmNDsLBIRe1MMRZCJPTdw/xhTD0IMUVO4sf3KqrzuRouGzwwLZn34+gFeVXiUQA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + '@next/swc-win32-arm64-msvc@15.5.24': resolution: {integrity: sha512-jBDDkZ/qKAqkWivWDMkJSXUzbzV0QKRBKJjEHUAvSB97Hzw7NLzJ6yV56Lts/wjir7s4P31GYgpbS6ZL+hasAA==} engines: {node: '>= 10'} @@ -4268,6 +4314,12 @@ packages: cpu: [arm64] os: [win32] + '@next/swc-win32-arm64-msvc@16.4.6': + resolution: {integrity: sha512-U0p7/tEtqvHX2crhd841CDwKtp5CFkKXVjOhmfccR3Pvp64sj8KBwRqsvltqLOwVBbj0NtkxwXSDpEHYGbxeXQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + '@next/swc-win32-x64-msvc@15.5.24': resolution: {integrity: sha512-JqtwjvvorjacQ0spgjmUJoxySoYgPwdT1sFdQ0/zmW4iMlP2hjYlCoJIyS7o6Epb4Fug8eco3HXFoBDUCDeH7Q==} engines: {node: '>= 10'} @@ -4280,6 +4332,12 @@ packages: cpu: [x64] os: [win32] + '@next/swc-win32-x64-msvc@16.4.6': + resolution: {integrity: sha512-3ZG15/5WIdlTQsIEvc9sPTTSL5XFrlLklIKmfF4fwsDnoAhu6NcBDXsk3mbGxNyo6aVLEGex+fHCv2MahY7a9g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==} @@ -11871,6 +11929,27 @@ packages: sass: optional: true + next@16.4.6: + resolution: {integrity: sha512-QZJiYhaZLJMo2KpkQGQIwvmd79WVsAWeYIU554a8d7zArS3si/IRy3lBNV/rtYhE70oLwdLYhPAXLPx8HwcwtA==} + engines: {node: '>=20.9.0'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.1.0 + '@playwright/test': ^1.51.1 + babel-plugin-react-compiler: '*' + react: 18.3.1 + react-dom: 18.3.1 + sass: ^1.3.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@playwright/test': + optional: true + babel-plugin-react-compiler: + optional: true + sass: + optional: true + nitropack@2.13.4: resolution: {integrity: sha512-tX7bT6zxNeMwkc6hxHiZeUoTOjVrcjoh1Z3cmxOlodIqjl4HISgqfGOmkWSayky3Nv9Z5+KQH52F8nmXJY5AAA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18218,7 +18297,7 @@ snapshots: dependencies: '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.20.1 + '@types/node': 24.19.2 '@types/yargs': 15.0.20 chalk: 4.1.2 optional: true @@ -18712,6 +18791,8 @@ snapshots: '@next/env@16.4.5': {} + '@next/env@16.4.6': {} + '@next/mdx@16.4.3(@mdx-js/loader@3.1.1(supports-color@10.2.2)(webpack@5.102.1(esbuild@0.27.7)(uglify-js@3.19.3)))(@mdx-js/react@3.1.1(@types/react@18.3.28)(react@18.3.1))': dependencies: source-map: 0.7.6 @@ -18725,48 +18806,72 @@ snapshots: '@next/swc-darwin-arm64@16.4.5': optional: true + '@next/swc-darwin-arm64@16.4.6': + optional: true + '@next/swc-darwin-x64@15.5.24': optional: true '@next/swc-darwin-x64@16.4.5': optional: true + '@next/swc-darwin-x64@16.4.6': + optional: true + '@next/swc-linux-arm64-gnu@15.5.24': optional: true '@next/swc-linux-arm64-gnu@16.4.5': optional: true + '@next/swc-linux-arm64-gnu@16.4.6': + optional: true + '@next/swc-linux-arm64-musl@15.5.24': optional: true '@next/swc-linux-arm64-musl@16.4.5': optional: true + '@next/swc-linux-arm64-musl@16.4.6': + optional: true + '@next/swc-linux-x64-gnu@15.5.24': optional: true '@next/swc-linux-x64-gnu@16.4.5': optional: true + '@next/swc-linux-x64-gnu@16.4.6': + optional: true + '@next/swc-linux-x64-musl@15.5.24': optional: true '@next/swc-linux-x64-musl@16.4.5': optional: true + '@next/swc-linux-x64-musl@16.4.6': + optional: true + '@next/swc-win32-arm64-msvc@15.5.24': optional: true '@next/swc-win32-arm64-msvc@16.4.5': optional: true + '@next/swc-win32-arm64-msvc@16.4.6': + optional: true + '@next/swc-win32-x64-msvc@15.5.24': optional: true '@next/swc-win32-x64-msvc@16.4.5': optional: true + '@next/swc-win32-x64-msvc@16.4.6': + optional: true + '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': optional: true @@ -21431,7 +21536,7 @@ snapshots: '@types/ws@7.4.7': dependencies: - '@types/node': 22.20.1 + '@types/node': 12.20.55 '@types/ws@8.18.1': dependencies: @@ -26603,7 +26708,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 22.20.1 + '@types/node': 24.19.2 merge-stream: 2.0.0 supports-color: 8.1.1 optional: true @@ -28309,6 +28414,34 @@ snapshots: - '@types/node' - babel-plugin-macros + next@16.4.6(@babel/core@7.29.7(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.62.1)(@types/node@22.20.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@next/env': 16.4.6 + '@swc/helpers': 0.5.23 + baseline-browser-mapping: 2.11.20 + caniuse-lite: 1.0.30001810 + postcss: 8.5.23 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@10.2.2))(babel-plugin-macros@3.1.0)(react@18.3.1) + optionalDependencies: + '@next/swc-darwin-arm64': 16.4.6 + '@next/swc-darwin-x64': 16.4.6 + '@next/swc-linux-arm64-gnu': 16.4.6 + '@next/swc-linux-arm64-musl': 16.4.6 + '@next/swc-linux-x64-gnu': 16.4.6 + '@next/swc-linux-x64-musl': 16.4.6 + '@next/swc-win32-arm64-msvc': 16.4.6 + '@next/swc-win32-x64-msvc': 16.4.6 + '@opentelemetry/api': 1.9.0 + '@playwright/test': 1.62.1 + babel-plugin-react-compiler: 1.0.0 + sharp: 0.35.5(@types/node@22.20.1) + transitivePeerDependencies: + - '@babel/core' + - '@types/node' + - babel-plugin-macros + nitropack@2.13.4(@rspack/core@2.0.6(@swc/helpers@0.5.23))(idb-keyval@6.2.1)(rolldown@1.1.0)(supports-color@10.2.2)(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.23.14)(yaml@2.9.0))(webpack@5.102.1(esbuild@0.28.1)(uglify-js@3.19.3)): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 @@ -30192,7 +30325,7 @@ snapshots: rpc-websockets@9.3.8: dependencies: - '@swc/helpers': 0.5.21 + '@swc/helpers': 0.5.23 '@types/uuid': 10.0.0 '@types/ws': 8.18.1 buffer: 6.0.3 From 27d2f6991131e97812d312346c747643ea50a1e0 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 12 Nov 2026 15:13:54 -0500 Subject: [PATCH 3/7] test(mosaic): run the Server Component smoke test in CI --- .changeset/mosaic-rsc-compound-components.md | 5 +++++ .github/workflows/ci.yml | 3 +++ .prettierignore | 1 + .../src/components/dialog/__tests__/alert-dialog.test.tsx | 2 +- .../mosaic/src/components/dialog/__tests__/dialog.test.tsx | 2 +- .../mosaic/src/components/input-group/input-group.test.tsx | 2 +- packages/mosaic/src/components/profile/profile.test.tsx | 6 +++--- 7 files changed, 15 insertions(+), 6 deletions(-) create mode 100644 .changeset/mosaic-rsc-compound-components.md diff --git a/.changeset/mosaic-rsc-compound-components.md b/.changeset/mosaic-rsc-compound-components.md new file mode 100644 index 00000000000..05f68a2d186 --- /dev/null +++ b/.changeset/mosaic-rsc-compound-components.md @@ -0,0 +1,5 @@ +--- +'@clerk/mosaic': minor +--- + +Compound components such as `Avatar`, `Banner`, `DataList`, `EmptyState`, `Entity` and `Item` can now be rendered from React Server Components, e.g. `` in a Next.js App Router page. Each compound export is now a module namespace object instead of a plain object, so `X.Part` usage is unchanged, but its parts are read-only: assigning to `X.Part` no longer replaces it. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 389b299907f..f4b7c4148df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -427,6 +427,9 @@ jobs: - name: Run feature tests run: pnpm --filter @clerk/mosaic test:feature + - name: Run Server Component smoke test + run: pnpm --filter @clerk/mosaic test:rsc + - name: Run browser tests run: pnpm --filter @clerk/mosaic test:browser diff --git a/.prettierignore b/.prettierignore index 198caa9919a..a56a3923e03 100644 --- a/.prettierignore +++ b/.prettierignore @@ -40,3 +40,4 @@ integration/expo-native/test/*.ts !integration/expo-native/test/remote-host.test.ts integration/expo-native/.verify/ integration/expo-native/specs/explored/ +packages/mosaic/test/rsc/app/page.jsx diff --git a/packages/mosaic/src/components/dialog/__tests__/alert-dialog.test.tsx b/packages/mosaic/src/components/dialog/__tests__/alert-dialog.test.tsx index 00bbba5a3ec..24c2c352fff 100644 --- a/packages/mosaic/src/components/dialog/__tests__/alert-dialog.test.tsx +++ b/packages/mosaic/src/components/dialog/__tests__/alert-dialog.test.tsx @@ -4,8 +4,8 @@ import React from 'react'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { Card } from '../../card'; -import type { DialogRootProps } from '../dialog'; import { Dialog } from '..'; +import type { DialogRootProps } from '../dialog'; afterEach(() => cleanup()); diff --git a/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx b/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx index 667752af551..f3aa49fec7e 100644 --- a/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx +++ b/packages/mosaic/src/components/dialog/__tests__/dialog.test.tsx @@ -9,8 +9,8 @@ import { colorVars, radiusVars, space } from '../../../tokens.stylex'; import { Button } from '../../button'; import { ButtonContext } from '../../button/button.context'; import { Card } from '../../card'; -import type { DialogVariant } from '../dialog'; import { Dialog } from '..'; +import type { DialogVariant } from '../dialog'; import { styles as dialogStyles } from '../dialog.styles'; afterEach(() => cleanup()); diff --git a/packages/mosaic/src/components/input-group/input-group.test.tsx b/packages/mosaic/src/components/input-group/input-group.test.tsx index 4259ccb8ac6..53f37ae3344 100644 --- a/packages/mosaic/src/components/input-group/input-group.test.tsx +++ b/packages/mosaic/src/components/input-group/input-group.test.tsx @@ -129,7 +129,7 @@ describe('Mosaic InputGroup', () => { }); it.each(['Start', 'End'] as const)('provides button defaults inside %s', side => { - const Slot = InputGroup[side]; + const Slot = { Start: InputGroup.Start, End: InputGroup.End }[side]; render( diff --git a/packages/mosaic/src/components/profile/profile.test.tsx b/packages/mosaic/src/components/profile/profile.test.tsx index 0bd64d7d0a0..dfedde448db 100644 --- a/packages/mosaic/src/components/profile/profile.test.tsx +++ b/packages/mosaic/src/components/profile/profile.test.tsx @@ -14,8 +14,8 @@ import { HeadingLevelProvider } from '../heading'; import { Icon } from '../icon'; import { Panel, usePanelTitle } from '../panel'; import { Section } from '../section'; -import type { ProfileRootProps } from './profile'; import { Profile } from '.'; +import type { ProfileRootProps } from './profile'; const pageTitles: Record = { account: 'Account', security: 'Security' }; @@ -369,13 +369,13 @@ describe('Profile', () => { observe = width => { Object.defineProperty(target, 'offsetWidth', { configurable: true, value: width }); Object.defineProperty(target, 'offsetHeight', { configurable: true, value: 600 }); - this.callback([], this as unknown as ResizeObserver); + this.callback([], this); }; } disconnect() {} unobserve() {} } - globalThis.ResizeObserver = FakeResizeObserver as unknown as typeof ResizeObserver; + globalThis.ResizeObserver = FakeResizeObserver; }); afterEach(() => { From 3ca9db9579a5d09c4a1f34601c33710f3aa2edde Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 12 Nov 2026 15:23:52 -0500 Subject: [PATCH 4/7] fix(mosaic): pass Panel.Title refs straight to useRender --- packages/mosaic/src/components/panel/panel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mosaic/src/components/panel/panel.tsx b/packages/mosaic/src/components/panel/panel.tsx index 97f7b13fa61..ebbcb7c56ed 100644 --- a/packages/mosaic/src/components/panel/panel.tsx +++ b/packages/mosaic/src/components/panel/panel.tsx @@ -50,7 +50,7 @@ const Title = React.forwardRef(function PanelTi return useRender({ defaultTagName: 'div', render, - ref: pageTitleRef ? null : titleRef, + ref: [ref, panelTitleRef], enabled: !inProfilePage, props: { ...mergeStyleProps(themeProps('panel-title'), stylex.props(reset.base, styles.title, xstyle), { From fb58585ef9de15b6135580367f8ab79514af17b6 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 12 Nov 2026 15:29:53 -0500 Subject: [PATCH 5/7] test(mosaic): remove the Server Component smoke test --- .github/workflows/ci.yml | 3 - .prettierignore | 1 - packages/mosaic/.gitignore | 3 - packages/mosaic/package.json | 2 - packages/mosaic/scripts/test-rsc.mjs | 71 ---------- packages/mosaic/test/rsc/app/layout.jsx | 7 - packages/mosaic/test/rsc/app/unrendered.jsx | 5 - pnpm-lock.yaml | 143 +------------------- 8 files changed, 5 insertions(+), 230 deletions(-) delete mode 100644 packages/mosaic/scripts/test-rsc.mjs delete mode 100644 packages/mosaic/test/rsc/app/layout.jsx delete mode 100644 packages/mosaic/test/rsc/app/unrendered.jsx diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4b7c4148df..389b299907f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -427,9 +427,6 @@ jobs: - name: Run feature tests run: pnpm --filter @clerk/mosaic test:feature - - name: Run Server Component smoke test - run: pnpm --filter @clerk/mosaic test:rsc - - name: Run browser tests run: pnpm --filter @clerk/mosaic test:browser diff --git a/.prettierignore b/.prettierignore index a56a3923e03..198caa9919a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -40,4 +40,3 @@ integration/expo-native/test/*.ts !integration/expo-native/test/remote-host.test.ts integration/expo-native/.verify/ integration/expo-native/specs/explored/ -packages/mosaic/test/rsc/app/page.jsx diff --git a/packages/mosaic/.gitignore b/packages/mosaic/.gitignore index 9b1788f5db0..21c3f208e04 100644 --- a/packages/mosaic/.gitignore +++ b/packages/mosaic/.gitignore @@ -1,4 +1 @@ .verify -test/rsc/.next -test/rsc/app/page.jsx -test/rsc/next-env.d.ts diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index f41050837f5..cbdceb37b72 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -132,7 +132,6 @@ "test:browser": "vitest run --project browser", "test:ci": "vitest run --project primitives --project mosaic --maxWorkers=70%", "test:feature": "vitest run --project feature", - "test:rsc": "node ./scripts/test-rsc.mjs", "test:visual": "bash ./scripts/test-visual.sh compare", "test:visual:update": "bash ./scripts/test-visual.sh update", "test:webkit": "vitest run --project webkit", @@ -161,7 +160,6 @@ "eslint-plugin-react-you-might-not-need-an-effect": "1.0.2", "happy-dom": "^20.8.9", "msw": "2.15.0", - "next": "^16.4.3", "playwright": "1.62.1", "qrcode.react": "4.2.0", "react": "catalog:react", diff --git a/packages/mosaic/scripts/test-rsc.mjs b/packages/mosaic/scripts/test-rsc.mjs deleted file mode 100644 index f00dd11515a..00000000000 --- a/packages/mosaic/scripts/test-rsc.mjs +++ /dev/null @@ -1,71 +0,0 @@ -import { spawnSync } from 'node:child_process'; -import { readFile, rm, writeFile } from 'node:fs/promises'; -import { createRequire } from 'node:module'; -import { resolve } from 'node:path'; - -const packageDir = resolve(import.meta.dirname, '..'); -const fixtureDir = resolve(packageDir, 'test/rsc'); -const packageJson = JSON.parse(await readFile(resolve(packageDir, 'package.json'), 'utf8')); - -const isNamespace = value => value !== null && typeof value === 'object' && !('$$typeof' in value); -const isComponent = (name, value) => - /^[A-Z]/.test(name) && - (typeof value === 'function' || - (value !== null && typeof value === 'object' && value.$$typeof !== Symbol.for('react.context'))); - -const entries = await Promise.all( - Object.entries(packageJson.exports) - .filter(([subpath]) => subpath.startsWith('./components/')) - .map(async ([subpath, target], index) => { - const module = await import(resolve(packageDir, target.import)); - return { specifier: `@clerk/mosaic${subpath.slice(1)}`, alias: `entry${index}`, module }; - }), -); - -const references = []; -const elements = []; -for (const { specifier, alias, module } of entries) { - for (const [name, value] of Object.entries(module)) { - if (isNamespace(value)) { - for (const part of Object.keys(value)) { - references.push(` ['${specifier}', '${name}.${part}', ${alias}.${name}.${part}],`); - if (isComponent(part, value[part])) elements.push(` <${alias}.${name}.${part} />`); - } - } else { - references.push(` ['${specifier}', '${name}', ${alias}.${name}],`); - if (isComponent(name, value)) elements.push(` <${alias}.${name} />`); - } - } -} - -const page = `import { Unrendered } from './unrendered'; -${entries.map(({ specifier, alias }) => `import * as ${alias} from '${specifier}';`).join('\n')} - -const references = [ -${references.join('\n')} -]; - -export default function Page() { - const missing = references.filter(([, , value]) => value === undefined).map(([specifier, name]) => \`\${specifier}: \${name}\`); - if (missing.length > 0) { - throw new Error(\`Undefined in a Server Component:\\n\${missing.join('\\n')}\`); - } - return ( - -${elements.join('\n')} - - ); -} -`; - -await writeFile(resolve(fixtureDir, 'app/page.jsx'), page); -await rm(resolve(fixtureDir, '.next'), { recursive: true, force: true }); - -const bundlerFlag = process.argv.includes('--webpack') ? ['--webpack'] : []; -const nextBin = createRequire(import.meta.url).resolve('next/dist/bin/next'); -const { status, error } = spawnSync(process.execPath, [nextBin, 'build', ...bundlerFlag, fixtureDir], { - cwd: packageDir, - stdio: 'inherit', -}); -if (error) throw error; -process.exit(status ?? 1); diff --git a/packages/mosaic/test/rsc/app/layout.jsx b/packages/mosaic/test/rsc/app/layout.jsx deleted file mode 100644 index 9af189d31d5..00000000000 --- a/packages/mosaic/test/rsc/app/layout.jsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function RootLayout({ children }) { - return ( - - {children} - - ); -} diff --git a/packages/mosaic/test/rsc/app/unrendered.jsx b/packages/mosaic/test/rsc/app/unrendered.jsx deleted file mode 100644 index f69bb9d72fe..00000000000 --- a/packages/mosaic/test/rsc/app/unrendered.jsx +++ /dev/null @@ -1,5 +0,0 @@ -'use client'; - -export function Unrendered() { - return null; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25c0562fa62..dc4fae29ab5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1033,9 +1033,6 @@ importers: msw: specifier: 2.15.0 version: 2.15.0(@types/node@22.20.1)(typescript@7.0.2) - next: - specifier: ^16.4.3 - version: 16.4.5(@babel/core@7.29.7(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.62.1)(@types/node@22.20.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) playwright: specifier: 1.62.1 version: 1.62.1 @@ -1068,7 +1065,7 @@ importers: version: 2.13.6(@types/node@22.20.1)(typescript@6.0.3) next: specifier: '>=15.5.18' - version: 16.4.6(@babel/core@7.29.7(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.62.1)(@types/node@22.20.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 16.4.5(@babel/core@7.29.7(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.62.1)(@types/node@22.20.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: 18.3.1 version: 18.3.1 @@ -4168,9 +4165,6 @@ packages: '@next/env@16.4.5': resolution: {integrity: sha512-JwSPwpdkZL5VMl2jA/q+JI8MZdweAL3xmw5/tb9y/TLtmS0YXZnmYDzSihwk8xTj1OL+WM53gTqaj96yuLp6Xw==} - '@next/env@16.4.6': - resolution: {integrity: sha512-SunBjJ6t+oBtecS3dTjdVuMhUogP5wD/vrGL5EVuQYM1iS4O5ilL51HkiJ3sPjMzRHiCChvr4dbaSJbR7F3lxA==} - '@next/mdx@16.4.3': resolution: {integrity: sha512-N86fgGEVCoB0ULnWlmRZcaZnZD5sLmBkZt4CAPxy0b04qpwimlCdhA6MnwdnJBqmByl2OEKHpQt/GpJRz3UUwQ==} peerDependencies: @@ -4194,12 +4188,6 @@ packages: cpu: [arm64] os: [darwin] - '@next/swc-darwin-arm64@16.4.6': - resolution: {integrity: sha512-kJ4VVZkley17xKaJx72QN2IBqjrj7zenCQZaUF0l9ShS/1Mt2kW4+Z5KGG6vgr3L/J562xTOizVyM1iA/u5nlg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - '@next/swc-darwin-x64@15.5.24': resolution: {integrity: sha512-9HrQajBMmGcrrrvDfRimiCrbAPh3E6uHJmwBovYr6Yrmi9p9PZqI876BrXX280wICh3o2XwUlp4blkB0NNBqFg==} engines: {node: '>= 10'} @@ -4212,12 +4200,6 @@ packages: cpu: [x64] os: [darwin] - '@next/swc-darwin-x64@16.4.6': - resolution: {integrity: sha512-eIPGZIqC+8JKaeZmvnCANJQF4xWkkHqSnAAajtFxgwDMAOnCgxNvP52SeWZ41PyVwoe+Le/NQuZSaQ8j7YshSA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - '@next/swc-linux-arm64-gnu@15.5.24': resolution: {integrity: sha512-rl9LSfE75si0WT3cDgdUC1XYCKS+TgxC+/IjitmeycrAG18X/plIP1/vy8dd/HPycYcIvE688PD7FuvEAiEAew==} engines: {node: '>= 10'} @@ -4232,13 +4214,6 @@ packages: os: [linux] libc: [glibc] - '@next/swc-linux-arm64-gnu@16.4.6': - resolution: {integrity: sha512-9nI6O8vvczH2D9ZNdMGqb/3NuT15T9ksOZnAmx9/gjF2cOi74+xxyf1Hk96uIMfm14c9y6wHeBq6SH8g6udvcA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [glibc] - '@next/swc-linux-arm64-musl@15.5.24': resolution: {integrity: sha512-TlNAnpsjxSF3aAUtqnfmtXXf8m9sIDBlmF3c7bTAlnshUYu2U0OxN2uf5d0gcFwqHVEdivJNBcCaqNOwPGNimw==} engines: {node: '>= 10'} @@ -4253,13 +4228,6 @@ packages: os: [linux] libc: [musl] - '@next/swc-linux-arm64-musl@16.4.6': - resolution: {integrity: sha512-7/qPhTxDp2ScluOJOhLuhWmlA17o1yZ0SIgpISBCpBxXgtnK4+KGywBpFkfLPiU1rnSM1faFGzYkH6i7JdTrqA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - libc: [musl] - '@next/swc-linux-x64-gnu@15.5.24': resolution: {integrity: sha512-7dwtlhr0SLndqTG1z9ncRkbJswDZiKWlxzFyXDvJ2RDZRDRHp8zyMJ4D9UH/FgnQeXxxB6gZy2pMcIUoNKQ4pA==} engines: {node: '>= 10'} @@ -4274,13 +4242,6 @@ packages: os: [linux] libc: [glibc] - '@next/swc-linux-x64-gnu@16.4.6': - resolution: {integrity: sha512-oOpbNa8XOW1qCGd9VvzC4fbkkRxEls7/WI6tz+YHC0xRA7KpV1U7DrAnCPjzVyOk/karzWEepCOcTIDmwK8FwQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [glibc] - '@next/swc-linux-x64-musl@15.5.24': resolution: {integrity: sha512-kGZxM+WhkYs0276lFrMkj7PRtXT3Btp6cwvfSO/cCVLxJJttB5Ccnl2niaCgUja8HgSbEVnMHpg3FJWoOJ9e/g==} engines: {node: '>= 10'} @@ -4295,13 +4256,6 @@ packages: os: [linux] libc: [musl] - '@next/swc-linux-x64-musl@16.4.6': - resolution: {integrity: sha512-aM8CZ+OR2EwXaAYw7KGkW2GDmNDsLBIRe1MMRZCJPTdw/xhTD0IMUVO4sf3KqrzuRouGzwwLZn34+gFeVXiUQA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [musl] - '@next/swc-win32-arm64-msvc@15.5.24': resolution: {integrity: sha512-jBDDkZ/qKAqkWivWDMkJSXUzbzV0QKRBKJjEHUAvSB97Hzw7NLzJ6yV56Lts/wjir7s4P31GYgpbS6ZL+hasAA==} engines: {node: '>= 10'} @@ -4314,12 +4268,6 @@ packages: cpu: [arm64] os: [win32] - '@next/swc-win32-arm64-msvc@16.4.6': - resolution: {integrity: sha512-U0p7/tEtqvHX2crhd841CDwKtp5CFkKXVjOhmfccR3Pvp64sj8KBwRqsvltqLOwVBbj0NtkxwXSDpEHYGbxeXQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - '@next/swc-win32-x64-msvc@15.5.24': resolution: {integrity: sha512-JqtwjvvorjacQ0spgjmUJoxySoYgPwdT1sFdQ0/zmW4iMlP2hjYlCoJIyS7o6Epb4Fug8eco3HXFoBDUCDeH7Q==} engines: {node: '>= 10'} @@ -4332,12 +4280,6 @@ packages: cpu: [x64] os: [win32] - '@next/swc-win32-x64-msvc@16.4.6': - resolution: {integrity: sha512-3ZG15/5WIdlTQsIEvc9sPTTSL5XFrlLklIKmfF4fwsDnoAhu6NcBDXsk3mbGxNyo6aVLEGex+fHCv2MahY7a9g==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': resolution: {integrity: sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==} @@ -11929,27 +11871,6 @@ packages: sass: optional: true - next@16.4.6: - resolution: {integrity: sha512-QZJiYhaZLJMo2KpkQGQIwvmd79WVsAWeYIU554a8d7zArS3si/IRy3lBNV/rtYhE70oLwdLYhPAXLPx8HwcwtA==} - engines: {node: '>=20.9.0'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.51.1 - babel-plugin-react-compiler: '*' - react: 18.3.1 - react-dom: 18.3.1 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@playwright/test': - optional: true - babel-plugin-react-compiler: - optional: true - sass: - optional: true - nitropack@2.13.4: resolution: {integrity: sha512-tX7bT6zxNeMwkc6hxHiZeUoTOjVrcjoh1Z3cmxOlodIqjl4HISgqfGOmkWSayky3Nv9Z5+KQH52F8nmXJY5AAA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -18297,7 +18218,7 @@ snapshots: dependencies: '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 24.19.2 + '@types/node': 22.20.1 '@types/yargs': 15.0.20 chalk: 4.1.2 optional: true @@ -18791,8 +18712,6 @@ snapshots: '@next/env@16.4.5': {} - '@next/env@16.4.6': {} - '@next/mdx@16.4.3(@mdx-js/loader@3.1.1(supports-color@10.2.2)(webpack@5.102.1(esbuild@0.27.7)(uglify-js@3.19.3)))(@mdx-js/react@3.1.1(@types/react@18.3.28)(react@18.3.1))': dependencies: source-map: 0.7.6 @@ -18806,72 +18725,48 @@ snapshots: '@next/swc-darwin-arm64@16.4.5': optional: true - '@next/swc-darwin-arm64@16.4.6': - optional: true - '@next/swc-darwin-x64@15.5.24': optional: true '@next/swc-darwin-x64@16.4.5': optional: true - '@next/swc-darwin-x64@16.4.6': - optional: true - '@next/swc-linux-arm64-gnu@15.5.24': optional: true '@next/swc-linux-arm64-gnu@16.4.5': optional: true - '@next/swc-linux-arm64-gnu@16.4.6': - optional: true - '@next/swc-linux-arm64-musl@15.5.24': optional: true '@next/swc-linux-arm64-musl@16.4.5': optional: true - '@next/swc-linux-arm64-musl@16.4.6': - optional: true - '@next/swc-linux-x64-gnu@15.5.24': optional: true '@next/swc-linux-x64-gnu@16.4.5': optional: true - '@next/swc-linux-x64-gnu@16.4.6': - optional: true - '@next/swc-linux-x64-musl@15.5.24': optional: true '@next/swc-linux-x64-musl@16.4.5': optional: true - '@next/swc-linux-x64-musl@16.4.6': - optional: true - '@next/swc-win32-arm64-msvc@15.5.24': optional: true '@next/swc-win32-arm64-msvc@16.4.5': optional: true - '@next/swc-win32-arm64-msvc@16.4.6': - optional: true - '@next/swc-win32-x64-msvc@15.5.24': optional: true '@next/swc-win32-x64-msvc@16.4.5': optional: true - '@next/swc-win32-x64-msvc@16.4.6': - optional: true - '@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3': optional: true @@ -21536,7 +21431,7 @@ snapshots: '@types/ws@7.4.7': dependencies: - '@types/node': 12.20.55 + '@types/node': 22.20.1 '@types/ws@8.18.1': dependencies: @@ -26708,7 +26603,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 24.19.2 + '@types/node': 22.20.1 merge-stream: 2.0.0 supports-color: 8.1.1 optional: true @@ -28414,34 +28309,6 @@ snapshots: - '@types/node' - babel-plugin-macros - next@16.4.6(@babel/core@7.29.7(supports-color@10.2.2))(@opentelemetry/api@1.9.0)(@playwright/test@1.62.1)(@types/node@22.20.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - '@next/env': 16.4.6 - '@swc/helpers': 0.5.23 - baseline-browser-mapping: 2.11.20 - caniuse-lite: 1.0.30001810 - postcss: 8.5.23 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - styled-jsx: 5.1.6(@babel/core@7.29.7(supports-color@10.2.2))(babel-plugin-macros@3.1.0)(react@18.3.1) - optionalDependencies: - '@next/swc-darwin-arm64': 16.4.6 - '@next/swc-darwin-x64': 16.4.6 - '@next/swc-linux-arm64-gnu': 16.4.6 - '@next/swc-linux-arm64-musl': 16.4.6 - '@next/swc-linux-x64-gnu': 16.4.6 - '@next/swc-linux-x64-musl': 16.4.6 - '@next/swc-win32-arm64-msvc': 16.4.6 - '@next/swc-win32-x64-msvc': 16.4.6 - '@opentelemetry/api': 1.9.0 - '@playwright/test': 1.62.1 - babel-plugin-react-compiler: 1.0.0 - sharp: 0.35.5(@types/node@22.20.1) - transitivePeerDependencies: - - '@babel/core' - - '@types/node' - - babel-plugin-macros - nitropack@2.13.4(@rspack/core@2.0.6(@swc/helpers@0.5.23))(idb-keyval@6.2.1)(rolldown@1.1.0)(supports-color@10.2.2)(vite@8.2.2(@types/node@22.20.1)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.46.1)(tsx@4.23.14)(yaml@2.9.0))(webpack@5.102.1(esbuild@0.28.1)(uglify-js@3.19.3)): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 @@ -30325,7 +30192,7 @@ snapshots: rpc-websockets@9.3.8: dependencies: - '@swc/helpers': 0.5.23 + '@swc/helpers': 0.5.21 '@types/uuid': 10.0.0 '@types/ws': 8.18.1 buffer: 6.0.3 From e2bc13fef2aa21d8e044d1bd66736f5110e9aef5 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 12 Nov 2026 15:33:55 -0500 Subject: [PATCH 6/7] chore(mosaic): measure the bundled index size in bundlewatch --- packages/mosaic/bundlewatch.config.json | 2 +- packages/mosaic/package.json | 3 ++- packages/mosaic/scripts/bundle-for-size.mjs | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 packages/mosaic/scripts/bundle-for-size.mjs diff --git a/packages/mosaic/bundlewatch.config.json b/packages/mosaic/bundlewatch.config.json index 358d736ab82..fa538516378 100644 --- a/packages/mosaic/bundlewatch.config.json +++ b/packages/mosaic/bundlewatch.config.json @@ -1,6 +1,6 @@ { "files": [ - { "path": "./dist/index.js", "maxSize": "95KB" }, + { "path": "./dist/.bundlewatch/index.js", "maxSize": "95KB" }, { "path": "./dist/styles.css", "maxSize": "18KB" } ] } diff --git a/packages/mosaic/package.json b/packages/mosaic/package.json index cbdceb37b72..c602b73f354 100644 --- a/packages/mosaic/package.json +++ b/packages/mosaic/package.json @@ -112,6 +112,7 @@ "dist", "!dist/**/*.d.ts.map", "!dist/**/*.d.mts.map", + "!dist/.bundlewatch", "styles.css.d.ts", "CHANGELOG.md" ], @@ -119,7 +120,7 @@ "build": "pnpm build:js && pnpm build:css && pnpm check:bundle && pnpm typecheck", "build:css": "tsdown --config tsdown.css.config.mts", "build:js": "tsdown --config tsdown.config.mts", - "bundlewatch": "FORCE_COLOR=1 bundlewatch --config bundlewatch.config.json", + "bundlewatch": "node ./scripts/bundle-for-size.mjs && FORCE_COLOR=1 bundlewatch --config bundlewatch.config.json", "check:bundle": "node ./scripts/check-bundle.mjs", "clean": "rimraf ./dist ./dist-css", "dev": "tsdown --config tsdown.config.mts --watch", diff --git a/packages/mosaic/scripts/bundle-for-size.mjs b/packages/mosaic/scripts/bundle-for-size.mjs new file mode 100644 index 00000000000..953090c16b6 --- /dev/null +++ b/packages/mosaic/scripts/bundle-for-size.mjs @@ -0,0 +1,16 @@ +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +import { build } from 'rolldown'; + +const packageDir = resolve(import.meta.dirname, '..'); +const packageJson = JSON.parse(await readFile(resolve(packageDir, 'package.json'), 'utf8')); +const externals = Object.keys({ ...packageJson.dependencies, ...packageJson.peerDependencies }); + +await build({ + input: resolve(packageDir, 'dist/index.js'), + platform: 'browser', + external: id => externals.some(name => id === name || id.startsWith(`${name}/`)), + output: { dir: resolve(packageDir, 'dist/.bundlewatch'), entryFileNames: '[name].js', format: 'esm' }, + logLevel: 'warn', +}); From 6733f4e65385d91c503414c4de1cdbd12f9fb9a0 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 12 Nov 2026 15:49:04 -0500 Subject: [PATCH 7/7] chore(mosaic): use a patch changeset --- .changeset/mosaic-rsc-compound-components.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/mosaic-rsc-compound-components.md b/.changeset/mosaic-rsc-compound-components.md index 05f68a2d186..662fdc65072 100644 --- a/.changeset/mosaic-rsc-compound-components.md +++ b/.changeset/mosaic-rsc-compound-components.md @@ -1,5 +1,5 @@ --- -'@clerk/mosaic': minor +'@clerk/mosaic': patch --- Compound components such as `Avatar`, `Banner`, `DataList`, `EmptyState`, `Entity` and `Item` can now be rendered from React Server Components, e.g. `` in a Next.js App Router page. Each compound export is now a module namespace object instead of a plain object, so `X.Part` usage is unchanged, but its parts are read-only: assigning to `X.Part` no longer replaces it.