diff --git a/packages/shared-components/.storybook/main.ts b/packages/shared-components/.storybook/main.ts
index 834cd6dc5b..a2ff26fb5a 100644
--- a/packages/shared-components/.storybook/main.ts
+++ b/packages/shared-components/.storybook/main.ts
@@ -49,6 +49,12 @@ const config: StorybookConfig = {
},
typescript: {
reactDocgen: "react-docgen-typescript",
+ reactDocgenTypescriptOptions: {
+ // The default exclude is ["**/**.stories.tsx"] which prevents
+ // docgen from extracting snapshot field descriptions from wrapper
+ // components defined in story files.
+ exclude: [],
+ },
},
async viteFinal(config) {
return mergeConfig(config, {
diff --git a/packages/shared-components/.storybook/withViewDocs.ts b/packages/shared-components/.storybook/withViewDocs.ts
new file mode 100644
index 0000000000..75385830ee
--- /dev/null
+++ b/packages/shared-components/.storybook/withViewDocs.ts
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2026 Element Creations Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
+ * Please see LICENSE files in the repository root for full details.
+ */
+
+/**
+ * Copies the component description and props documentation from a View's
+ * `__docgenInfo` (injected at build time by Storybook's react-docgen-typescript
+ * Vite plugin) onto the story wrapper component.
+ *
+ * This lets Storybook's default `extractComponentDescription` pick up the
+ * View's JSDoc and display per-field descriptions in the ArgTypes table.
+ *
+ * **Important:** the wrapper must be defined as a named variable *before*
+ * being passed here so that react-docgen-typescript can extract its props.
+ *
+ * @example
+ * ```ts
+ * const MyViewWrapperImpl = (props: MyViewProps) => {
+ * const vm = useMockedViewModel(props, {});
+ * return ;
+ * };
+ * const MyViewWrapper = withViewDocs(MyViewWrapperImpl, MyView);
+ * ```
+ */
+export function withViewDocs unknown>(wrapper: T, view: object): T {
+ const viewInfo = (view as { __docgenInfo?: DocgenInfo }).__docgenInfo;
+ const viewDescription = viewInfo?.description;
+ if (!viewDescription) return wrapper;
+
+ // The wrapper must be defined as a named variable (not inline) so that
+ // react-docgen-typescript can extract its props. The docgen Vite plugin
+ // appends a `Wrapper.__docgenInfo = { … }` assignment at the *end* of the
+ // module, which runs **after** this function. We install a setter trap so
+ // that the View's description is merged into the generated info.
+ let stored: DocgenInfo | undefined = (wrapper as { __docgenInfo?: DocgenInfo }).__docgenInfo;
+ Object.defineProperty(wrapper, "__docgenInfo", {
+ get() {
+ return stored;
+ },
+ set(incoming: DocgenInfo) {
+ stored = {
+ ...incoming,
+ description: incoming.description || viewDescription,
+ };
+ },
+ configurable: true,
+ enumerable: true,
+ });
+
+ // Also apply immediately for the current state.
+ stored = { ...stored, description: viewDescription };
+
+ return wrapper;
+}
+
+interface DocgenInfo {
+ description?: string;
+ props?: Record;
+}
diff --git a/packages/shared-components/README.md b/packages/shared-components/README.md
index 993450a65f..3653bf9d21 100644
--- a/packages/shared-components/README.md
+++ b/packages/shared-components/README.md
@@ -68,7 +68,7 @@ instance should be provided as a prop.
Here's a basic example:
-```jsx
+```tsx
import { ViewExample } from "@element-hq/web-shared-components";
function MyApp() {
@@ -180,27 +180,32 @@ export const Disabled: Story = {
#### MVVM Component Stories
-For MVVM components, create a wrapper component that uses `useMockedViewModel`:
+For MVVM components, create a wrapper component that uses `useMockedViewModel` and `withViewDocs`:
```tsx
import React, { type JSX } from "react";
import { fn } from "storybook/test";
-import type { Meta, StoryFn } from "@storybook/react-vite";
+import type { Meta, StoryObj } from "@storybook/react-vite";
import { MyComponentView, type MyComponentViewSnapshot, type MyComponentViewActions } from "./MyComponentView";
-import { useMockedViewModel } from "../../useMockedViewModel";
+import { useMockedViewModel } from "../../viewmodel";
+import { withViewDocs } from "../../../.storybook/withViewDocs";
// Combine snapshot and actions for easier typing
type MyComponentProps = MyComponentViewSnapshot & MyComponentViewActions;
-// Wrapper component that creates a mocked ViewModel
-const MyComponentViewWrapper = ({ onAction, ...rest }: MyComponentProps): JSX.Element => {
+// Wrapper component that creates a mocked ViewModel.
+// Must be a named variable (not inline) for docgen to extract its props.
+const MyComponentViewWrapperImpl = ({ onAction, ...rest }: MyComponentProps): JSX.Element => {
const vm = useMockedViewModel(rest, {
onAction,
});
return ;
};
+// withViewDocs copies the View's JSDoc description onto the wrapper for Storybook autodocs
+const MyComponentViewWrapper = withViewDocs(MyComponentViewWrapperImpl, MyComponentView);
-export default {
+// Must use `satisfies` (not `as` or `: Meta`) to preserve type info for docgen
+const meta = {
title: "Category/MyComponentView",
component: MyComponentViewWrapper,
tags: ["autodocs"],
@@ -211,20 +216,29 @@ export default {
// Action properties (callbacks)
onAction: fn(),
},
-} as Meta;
+} satisfies Meta;
-const Template: StoryFn = (args) => ;
+export default meta;
+type Story = StoryObj;
-export const Default = Template.bind({});
+export const Default: Story = {};
-export const Loading = Template.bind({});
-Loading.args = {
- isLoading: true,
+export const Loading: Story = {
+ args: {
+ isLoading: true,
+ },
};
```
Thanks to this approach, we can directly use primitives in the story arguments instead of a view model object.
+> [!IMPORTANT]
+> Three requirements must be met for snapshot field documentation to appear in Storybook's ArgTypes table:
+>
+> 1. **Named wrapper variable** — the wrapper must be assigned to a named `const` (e.g. `MyComponentViewWrapperImpl`) before being passed to `withViewDocs`, so that `react-docgen-typescript` can extract its props.
+> 2. **`withViewDocs` call** — wraps the wrapper component with the original View to copy the View's JSDoc description.
+> 3. **`satisfies Meta`** — the meta object must use `satisfies Meta<...>` (not `as Meta<...>` or `: Meta<...> =`). Type assertions and annotations erase the inferred component type that docgen relies on.
+
#### Linking Figma Designs
This package uses [@storybook/addon-designs](https://github.com/storybookjs/addon-designs) to embed Figma designs directly in Storybook. This helps developers compare their implementation with the design specs.
@@ -239,7 +253,7 @@ This package uses [@storybook/addon-designs](https://github.com/storybookjs/addo
Example with Figma integration:
```tsx
-export default {
+const meta = {
title: "Room List/RoomListSearchView",
component: RoomListSearchViewWrapper,
tags: ["autodocs"],
@@ -252,7 +266,9 @@ export default {
url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel?node-id=98-1979",
},
},
-} as Meta;
+} satisfies Meta;
+
+export default meta;
```
The Figma design will appear in the "Design" tab in Storybook.
diff --git a/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.stories.tsx b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.stories.tsx
index 8e2c91eadc..b81b79ea57 100644
--- a/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.stories.tsx
+++ b/packages/shared-components/src/audio/AudioPlayerView/AudioPlayerView.stories.tsx
@@ -8,12 +8,18 @@
import React, { type JSX } from "react";
import { fn } from "storybook/test";
-import type { Meta, StoryFn } from "@storybook/react-vite";
+import type { Meta, StoryObj } from "@storybook/react-vite";
import { AudioPlayerView, type AudioPlayerViewActions, type AudioPlayerViewSnapshot } from "./AudioPlayerView";
import { useMockedViewModel } from "../../viewmodel";
+import { withViewDocs } from "../../../.storybook/withViewDocs";
type AudioPlayerProps = AudioPlayerViewSnapshot & AudioPlayerViewActions;
-const AudioPlayerViewWrapper = ({ togglePlay, onKeyDown, onSeekbarChange, ...rest }: AudioPlayerProps): JSX.Element => {
+const AudioPlayerViewWrapperImpl = ({
+ togglePlay,
+ onKeyDown,
+ onSeekbarChange,
+ ...rest
+}: AudioPlayerProps): JSX.Element => {
const vm = useMockedViewModel(rest, {
togglePlay,
onKeyDown,
@@ -21,8 +27,9 @@ const AudioPlayerViewWrapper = ({ togglePlay, onKeyDown, onSeekbarChange, ...res
});
return ;
};
+const AudioPlayerViewWrapper = withViewDocs(AudioPlayerViewWrapperImpl, AudioPlayerView);
-export default {
+const meta = {
title: "Audio/AudioPlayerView",
component: AudioPlayerViewWrapper,
tags: ["autodocs"],
@@ -44,23 +51,27 @@ export default {
onKeyDown: fn(),
onSeekbarChange: fn(),
},
-} as Meta;
+} satisfies Meta;
-const Template: StoryFn = (args) => ;
+export default meta;
+type Story = StoryObj;
-export const Default = Template.bind({});
+export const Default: Story = {};
-export const NoMediaName = Template.bind({});
-NoMediaName.args = {
- mediaName: undefined,
+export const NoMediaName: Story = {
+ args: {
+ mediaName: undefined,
+ },
};
-export const NoSize = Template.bind({});
-NoSize.args = {
- sizeBytes: undefined,
+export const NoSize: Story = {
+ args: {
+ sizeBytes: undefined,
+ },
};
-export const HasError = Template.bind({});
-HasError.args = {
- error: true,
+export const HasError: Story = {
+ args: {
+ error: true,
+ },
};
diff --git a/packages/shared-components/src/event-tiles/EncryptionEventView/EncryptionEventView.stories.tsx b/packages/shared-components/src/event-tiles/EncryptionEventView/EncryptionEventView.stories.tsx
index 04066d1dbe..8a41e47adf 100644
--- a/packages/shared-components/src/event-tiles/EncryptionEventView/EncryptionEventView.stories.tsx
+++ b/packages/shared-components/src/event-tiles/EncryptionEventView/EncryptionEventView.stories.tsx
@@ -7,19 +7,21 @@
import React, { type JSX } from "react";
-import type { Meta, StoryFn } from "@storybook/react-vite";
+import type { Meta, StoryObj } from "@storybook/react-vite";
import { EncryptionEventView, EncryptionEventState, type EncryptionEventViewSnapshot } from "./EncryptionEventView";
import { useMockedViewModel } from "../../viewmodel/useMockedViewModel";
+import { withViewDocs } from "../../../.storybook/withViewDocs";
type EncryptionEventProps = EncryptionEventViewSnapshot;
-const EncryptionEventViewWrapper = ({ ...rest }: EncryptionEventProps): JSX.Element => {
+const EncryptionEventViewWrapperImpl = ({ ...rest }: EncryptionEventProps): JSX.Element => {
const vm = useMockedViewModel(rest, {});
return ;
};
+const EncryptionEventViewWrapper = withViewDocs(EncryptionEventViewWrapperImpl, EncryptionEventView);
-export default {
+const meta = {
title: "Event/EncryptionEvent",
component: EncryptionEventViewWrapper,
tags: ["autodocs"],
@@ -37,46 +39,54 @@ export default {
userName: "Alice",
className: "",
},
-} as Meta;
+} satisfies Meta;
-const Template: StoryFn = (args) => ;
+export default meta;
+type Story = StoryObj;
-export const Default = Template.bind({});
+export const Default: Story = {};
-export const StateEncryptionEnabled = Template.bind({});
-StateEncryptionEnabled.args = {
- state: EncryptionEventState.ENABLED,
- encryptedStateEvents: true,
+export const StateEncryptionEnabled: Story = {
+ args: {
+ state: EncryptionEventState.ENABLED,
+ encryptedStateEvents: true,
+ },
};
-export const ParametersChanged = Template.bind({});
-ParametersChanged.args = {
- state: EncryptionEventState.CHANGED,
+export const ParametersChanged: Story = {
+ args: {
+ state: EncryptionEventState.CHANGED,
+ },
};
-export const DisableAttempt = Template.bind({});
-DisableAttempt.args = {
- state: EncryptionEventState.DISABLE_ATTEMPT,
+export const DisableAttempt: Story = {
+ args: {
+ state: EncryptionEventState.DISABLE_ATTEMPT,
+ },
};
-export const EnabledDirectMessage = Template.bind({});
-EnabledDirectMessage.args = {
- state: EncryptionEventState.ENABLED_DM,
- userName: "Alice",
+export const EnabledDirectMessage: Story = {
+ args: {
+ state: EncryptionEventState.ENABLED_DM,
+ userName: "Alice",
+ },
};
-export const EnabledLocalRoom = Template.bind({});
-EnabledLocalRoom.args = {
- state: EncryptionEventState.ENABLED_LOCAL,
+export const EnabledLocalRoom: Story = {
+ args: {
+ state: EncryptionEventState.ENABLED_LOCAL,
+ },
};
-export const Unsupported = Template.bind({});
-Unsupported.args = {
- state: EncryptionEventState.UNSUPPORTED,
+export const Unsupported: Story = {
+ args: {
+ state: EncryptionEventState.UNSUPPORTED,
+ },
};
-export const WithTimestamp = Template.bind({});
-WithTimestamp.args = {
- state: EncryptionEventState.ENABLED,
- timestamp: 14:56,
+export const WithTimestamp: Story = {
+ args: {
+ state: EncryptionEventState.ENABLED,
+ timestamp: 14:56,
+ },
};
diff --git a/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx b/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx
index 741f7420de..590583a9f8 100644
--- a/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx
+++ b/packages/shared-components/src/message-body/DecryptionFailureBodyView/DecryptionFailureBodyView.stories.tsx
@@ -7,23 +7,25 @@
import React, { type JSX } from "react";
-import type { Meta, StoryFn } from "@storybook/react-vite";
+import type { Meta, StoryObj } from "@storybook/react-vite";
import {
DecryptionFailureBodyView,
DecryptionFailureReason,
type DecryptionFailureBodyViewSnapshot,
} from "./DecryptionFailureBodyView";
import { useMockedViewModel } from "../../viewmodel/useMockedViewModel";
+import { withViewDocs } from "../../../.storybook/withViewDocs";
type DecryptionFailureBodyProps = DecryptionFailureBodyViewSnapshot;
-const DecryptionFailureBodyViewWrapper = ({ ...rest }: DecryptionFailureBodyProps): JSX.Element => {
+const DecryptionFailureBodyViewWrapperImpl = ({ ...rest }: DecryptionFailureBodyProps): JSX.Element => {
const vm = useMockedViewModel(rest, {});
return ;
};
+const DecryptionFailureBodyViewWrapper = withViewDocs(DecryptionFailureBodyViewWrapperImpl, DecryptionFailureBodyView);
-export default {
+const meta = {
title: "MessageBody/DecryptionFailureBodyView",
component: DecryptionFailureBodyViewWrapper,
tags: ["autodocs"],
@@ -40,42 +42,46 @@ export default {
isLocalDeviceVerified: true,
extraClassNames: ["extra_class"],
},
-} as Meta;
+} satisfies Meta;
-const Template: StoryFn = (args) => (
-
-);
+export default meta;
+type Story = StoryObj;
-export const Default = Template.bind({});
+export const Default: Story = {};
-export const HasExtraClassNames = Template.bind({});
-HasExtraClassNames.args = {
- decryptionFailureReason: DecryptionFailureReason.UNABLE_TO_DECRYPT,
- extraClassNames: ["extra_class_1", "extra_class_2"],
+export const HasExtraClassNames: Story = {
+ args: {
+ decryptionFailureReason: DecryptionFailureReason.UNABLE_TO_DECRYPT,
+ extraClassNames: ["extra_class_1", "extra_class_2"],
+ },
};
-export const HasErrorClassName = Template.bind({});
-HasErrorClassName.args = {
- decryptionFailureReason: DecryptionFailureReason.UNSIGNED_SENDER_DEVICE,
- extraClassNames: undefined,
+export const HasErrorClassName: Story = {
+ args: {
+ decryptionFailureReason: DecryptionFailureReason.UNSIGNED_SENDER_DEVICE,
+ extraClassNames: undefined,
+ },
};
-export const HasErrorBlockIcon = Template.bind({});
-HasErrorBlockIcon.args = {
- decryptionFailureReason: DecryptionFailureReason.SENDER_IDENTITY_PREVIOUSLY_VERIFIED,
- extraClassNames: undefined,
+export const HasErrorBlockIcon: Story = {
+ args: {
+ decryptionFailureReason: DecryptionFailureReason.SENDER_IDENTITY_PREVIOUSLY_VERIFIED,
+ extraClassNames: undefined,
+ },
};
-export const HasBackupConfiguredVerifiedFalse = Template.bind({});
-HasBackupConfiguredVerifiedFalse.args = {
- decryptionFailureReason: DecryptionFailureReason.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED,
- isLocalDeviceVerified: false,
- extraClassNames: undefined,
+export const HasBackupConfiguredVerifiedFalse: Story = {
+ args: {
+ decryptionFailureReason: DecryptionFailureReason.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED,
+ isLocalDeviceVerified: false,
+ extraClassNames: undefined,
+ },
};
-export const HasBackupConfiguredVerifiedTrue = Template.bind({});
-HasBackupConfiguredVerifiedTrue.args = {
- decryptionFailureReason: DecryptionFailureReason.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED,
- isLocalDeviceVerified: true,
- extraClassNames: undefined,
+export const HasBackupConfiguredVerifiedTrue: Story = {
+ args: {
+ decryptionFailureReason: DecryptionFailureReason.HISTORICAL_MESSAGE_BACKUP_UNCONFIGURED,
+ isLocalDeviceVerified: true,
+ extraClassNames: undefined,
+ },
};
diff --git a/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.stories.tsx b/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.stories.tsx
index e512012a57..13077b6c03 100644
--- a/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.stories.tsx
+++ b/packages/shared-components/src/message-body/MessageTimestampView/MessageTimestampView.stories.tsx
@@ -8,24 +8,26 @@
import React, { type ReactNode } from "react";
import { expect, userEvent, within } from "storybook/test";
-import type { Meta, StoryFn } from "@storybook/react-vite";
+import type { Meta, StoryObj } from "@storybook/react-vite";
import {
MessageTimestampView,
type MessageTimestampViewActions,
type MessageTimestampViewSnapshot,
} from "./MessageTimestampView";
import { useMockedViewModel } from "../../viewmodel/useMockedViewModel";
+import { withViewDocs } from "../../../.storybook/withViewDocs";
type MessageTimestampProps = MessageTimestampViewSnapshot & MessageTimestampViewActions;
-const MessageTimestampWrapper = ({ onClick, onContextMenu, ...rest }: MessageTimestampProps): ReactNode => {
+const MessageTimestampWrapperImpl = ({ onClick, onContextMenu, ...rest }: MessageTimestampProps): ReactNode => {
const vm = useMockedViewModel(rest, {
onClick,
onContextMenu,
});
return ;
};
+const MessageTimestampWrapper = withViewDocs(MessageTimestampWrapperImpl, MessageTimestampView);
-export default {
+const meta = {
title: "MessageBody/MessageTimestamp",
component: MessageTimestampWrapper,
tags: ["autodocs"],
@@ -37,44 +39,51 @@ export default {
className: "",
href: "",
},
-} as Meta;
+} satisfies Meta;
-const Template: StoryFn = (args) => ;
+export default meta;
+type Story = StoryObj;
-export const Default = Template.bind({});
-Default.play = async ({ canvasElement }) => {
- const canvas = within(canvasElement);
- await userEvent.hover(canvas.getByText("04:58"));
- await expect(within(canvasElement.ownerDocument.body).findByRole("tooltip")).resolves.toBeInTheDocument();
+export const Default: Story = {
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ await userEvent.hover(canvas.getByText("04:58"));
+ await expect(within(canvasElement.ownerDocument.body).findByRole("tooltip")).resolves.toBeInTheDocument();
+ },
};
-export const HasTsReceivedAt = Template.bind({});
-HasTsReceivedAt.args = {
- tsReceivedAt: "Thu, 17 Nov 2022, 4:58:33 pm",
-};
-HasTsReceivedAt.play = async ({ canvasElement }) => {
- const canvas = within(canvasElement);
- await userEvent.hover(canvas.getByText("04:58"));
- await expect(within(canvasElement.ownerDocument.body).findByRole("tooltip")).resolves.toBeInTheDocument();
+export const HasTsReceivedAt: Story = {
+ args: {
+ tsReceivedAt: "Thu, 17 Nov 2022, 4:58:33 pm",
+ },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ await userEvent.hover(canvas.getByText("04:58"));
+ await expect(within(canvasElement.ownerDocument.body).findByRole("tooltip")).resolves.toBeInTheDocument();
+ },
};
-export const HasInhibitTooltip = Template.bind({});
-HasInhibitTooltip.args = {
- inhibitTooltip: true,
+export const HasInhibitTooltip: Story = {
+ args: {
+ inhibitTooltip: true,
+ },
};
-export const HasExtraClassNames = Template.bind({});
-HasExtraClassNames.args = {
- className: "extra_class_1 extra_class_2",
+export const HasExtraClassNames: Story = {
+ args: {
+ className: "extra_class_1 extra_class_2",
+ },
};
-export const HasHref = Template.bind({});
-HasHref.args = {
- href: "~",
+export const HasHref: Story = {
+ args: {
+ href: "~",
+ },
};
-export const HasActions = Template.bind({});
-HasActions.args = {
- onClick: () => console.log("Clicked message timestamp"),
- onContextMenu: () => console.log("Context menu on message timestamp"),
+export const HasActions: Story = {
+ args: {
+ onClick: () => console.log("Clicked message timestamp"),
+ onContextMenu: () => console.log("Context menu on message timestamp"),
+ },
};
diff --git a/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx b/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx
index ff8fa315b5..4d5095e334 100644
--- a/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx
+++ b/packages/shared-components/src/message-body/ReactionsRowButtonTooltip/ReactionsRowButtonTooltip.stories.tsx
@@ -7,8 +7,9 @@
import React, { type JSX, type PropsWithChildren } from "react";
-import type { Meta, StoryFn } from "@storybook/react-vite";
+import type { Meta, StoryObj } from "@storybook/react-vite";
import { useMockedViewModel } from "../../viewmodel";
+import { withViewDocs } from "../../../.storybook/withViewDocs";
import {
ReactionsRowButtonTooltipView,
type ReactionsRowButtonTooltipViewSnapshot,
@@ -16,12 +17,16 @@ import {
type WrapperProps = ReactionsRowButtonTooltipViewSnapshot & PropsWithChildren;
-const ReactionsRowButtonTooltipViewWrapper = ({ children, ...snapshotProps }: WrapperProps): JSX.Element => {
+const ReactionsRowButtonTooltipViewWrapperImpl = ({ children, ...snapshotProps }: WrapperProps): JSX.Element => {
const vm = useMockedViewModel(snapshotProps, {});
return {children};
};
+const ReactionsRowButtonTooltipViewWrapper = withViewDocs(
+ ReactionsRowButtonTooltipViewWrapperImpl,
+ ReactionsRowButtonTooltipView,
+);
-export default {
+const meta = {
title: "MessageBody/ReactionsRowButtonTooltip",
component: ReactionsRowButtonTooltipViewWrapper,
tags: ["autodocs"],
@@ -32,38 +37,41 @@ export default {
args: {
children: ,
},
-} as Meta;
+} satisfies Meta;
-const Template: StoryFn = (args) => (
-
-);
+export default meta;
+type Story = StoryObj;
-export const Default = Template.bind({});
-Default.args = {
- formattedSenders: "Alice, Bob and Charlie",
- caption: ":thumbsup:",
- tooltipOpen: true,
+export const Default: Story = {
+ args: {
+ formattedSenders: "Alice, Bob and Charlie",
+ caption: ":thumbsup:",
+ tooltipOpen: true,
+ },
};
-export const ManySenders = Template.bind({});
-ManySenders.args = {
- formattedSenders: "Alice, Bob, Charlie, David, Eve, Frank and 2 others",
- caption: ":heart:",
- children: ,
- tooltipOpen: true,
+export const ManySenders: Story = {
+ args: {
+ formattedSenders: "Alice, Bob, Charlie, David, Eve, Frank and 2 others",
+ caption: ":heart:",
+ children: ,
+ tooltipOpen: true,
+ },
};
-export const WithoutCaption = Template.bind({});
-WithoutCaption.args = {
- formattedSenders: "Alice and Bob",
- caption: undefined,
- children: ,
- tooltipOpen: true,
+export const WithoutCaption: Story = {
+ args: {
+ formattedSenders: "Alice and Bob",
+ caption: undefined,
+ children: ,
+ tooltipOpen: true,
+ },
};
-export const NoTooltip = Template.bind({});
-NoTooltip.args = {
- formattedSenders: undefined,
- caption: undefined,
- children: ,
+export const NoTooltip: Story = {
+ args: {
+ formattedSenders: undefined,
+ caption: undefined,
+ children: ,
+ },
};
diff --git a/packages/shared-components/src/profile/DisambiguatedProfile/DisambiguatedProfile.stories.tsx b/packages/shared-components/src/profile/DisambiguatedProfile/DisambiguatedProfile.stories.tsx
index 1f9128d3e0..b3231a5f75 100644
--- a/packages/shared-components/src/profile/DisambiguatedProfile/DisambiguatedProfile.stories.tsx
+++ b/packages/shared-components/src/profile/DisambiguatedProfile/DisambiguatedProfile.stories.tsx
@@ -8,22 +8,24 @@
import React, { type JSX } from "react";
import { fn } from "storybook/test";
-import type { Meta, StoryFn } from "@storybook/react-vite";
+import type { Meta, StoryObj } from "@storybook/react-vite";
import {
DisambiguatedProfileView,
type DisambiguatedProfileViewSnapshot,
type DisambiguatedProfileViewActions,
} from "./DisambiguatedProfileView";
import { useMockedViewModel } from "../../viewmodel";
+import { withViewDocs } from "../../../.storybook/withViewDocs";
type DisambiguatedProfileProps = DisambiguatedProfileViewSnapshot & DisambiguatedProfileViewActions;
-const DisambiguatedProfileViewWrapper = ({ onClick, ...rest }: DisambiguatedProfileProps): JSX.Element => {
+const DisambiguatedProfileViewWrapperImpl = ({ onClick, ...rest }: DisambiguatedProfileProps): JSX.Element => {
const vm = useMockedViewModel(rest, { onClick });
return ;
};
+const DisambiguatedProfileViewWrapper = withViewDocs(DisambiguatedProfileViewWrapperImpl, DisambiguatedProfileView);
-export default {
+const meta = {
title: "Profile/DisambiguatedProfile",
component: DisambiguatedProfileViewWrapper,
tags: ["autodocs"],
@@ -40,44 +42,48 @@ export default {
emphasizeDisplayName: true,
onClick: fn(),
},
-} as Meta;
+} satisfies Meta;
-const Template: StoryFn = (args) => (
-
-);
+export default meta;
+type Story = StoryObj;
-export const Default = Template.bind({});
+export const Default: Story = {};
-export const WithMxid = Template.bind({});
-WithMxid.args = {
- displayName: "Alice",
- displayIdentifier: "@alice:example.org",
- colorClass: "mx_Username_color1",
+export const WithMxid: Story = {
+ args: {
+ displayName: "Alice",
+ displayIdentifier: "@alice:example.org",
+ colorClass: "mx_Username_color1",
+ },
};
-export const WithColorClass = Template.bind({});
-WithColorClass.args = {
- displayName: "Bob",
- colorClass: "mx_Username_color3",
+export const WithColorClass: Story = {
+ args: {
+ displayName: "Bob",
+ colorClass: "mx_Username_color3",
+ },
};
-export const Emphasized = Template.bind({});
-Emphasized.args = {
- displayName: "Charlie",
- emphasizeDisplayName: true,
+export const Emphasized: Story = {
+ args: {
+ displayName: "Charlie",
+ emphasizeDisplayName: true,
+ },
};
-export const WithTooltip = Template.bind({});
-WithTooltip.args = {
- displayName: "Diana",
- title: "Diana (@diana:example.org)",
+export const WithTooltip: Story = {
+ args: {
+ displayName: "Diana",
+ title: "Diana (@diana:example.org)",
+ },
};
-export const FullExample = Template.bind({});
-FullExample.args = {
- displayName: "Eve",
- displayIdentifier: "@eve:matrix.org",
- colorClass: "mx_Username_color5",
- title: "Eve (@eve:matrix.org)",
- emphasizeDisplayName: true,
+export const FullExample: Story = {
+ args: {
+ displayName: "Eve",
+ displayIdentifier: "@eve:matrix.org",
+ colorClass: "mx_Username_color5",
+ title: "Eve (@eve:matrix.org)",
+ emphasizeDisplayName: true,
+ },
};
diff --git a/packages/shared-components/src/right-panel/WidgetContextMenu/WidgetContextMenuView.stories.tsx b/packages/shared-components/src/right-panel/WidgetContextMenu/WidgetContextMenuView.stories.tsx
index 87d3515d0c..3e3c2daeeb 100644
--- a/packages/shared-components/src/right-panel/WidgetContextMenu/WidgetContextMenuView.stories.tsx
+++ b/packages/shared-components/src/right-panel/WidgetContextMenu/WidgetContextMenuView.stories.tsx
@@ -10,17 +10,18 @@ import { fn } from "storybook/test";
import { IconButton } from "@vector-im/compound-web";
import TriggerIcon from "@vector-im/compound-design-tokens/assets/web/icons/overflow-horizontal";
-import type { Meta, StoryFn } from "@storybook/react-vite";
+import type { Meta, StoryObj } from "@storybook/react-vite";
import {
type WidgetContextMenuAction,
type WidgetContextMenuSnapshot,
WidgetContextMenuView,
} from "./WidgetContextMenuView";
import { useMockedViewModel } from "../../viewmodel/useMockedViewModel";
+import { withViewDocs } from "../../../.storybook/withViewDocs";
type WidgetContextMenuViewModelProps = WidgetContextMenuSnapshot & WidgetContextMenuAction;
-const WidgetContextMenuViewWrapper = ({
+const WidgetContextMenuViewWrapperImpl = ({
onStreamAudioClick,
onEditClick,
onSnapshotClick,
@@ -41,8 +42,9 @@ const WidgetContextMenuViewWrapper = ({
});
return ;
};
+const WidgetContextMenuViewWrapper = withViewDocs(WidgetContextMenuViewWrapperImpl, WidgetContextMenuView);
-export default {
+const meta = {
title: "RightPanel/WidgetContextMenuView",
component: WidgetContextMenuViewWrapper,
tags: ["autodocs"],
@@ -54,7 +56,6 @@ export default {
showSnapshotButton: true,
showMoveButtons: [true, true],
canModify: true,
- widgetMessaging: undefined,
isMenuOpened: true,
trigger: (
@@ -69,16 +70,18 @@ export default {
onFinished: fn(),
onMoveButton: fn(),
},
-} as Meta;
+} satisfies Meta;
-const Template: StoryFn = (args) => ;
+export default meta;
+type Story = StoryObj;
-export const Default = Template.bind({});
+export const Default: Story = {};
-export const OnlyBasicModification = Template.bind({});
-OnlyBasicModification.args = {
- showSnapshotButton: false,
- showMoveButtons: [false, false],
- showStreamAudioStreamButton: false,
- showEditButton: false,
+export const OnlyBasicModification: Story = {
+ args: {
+ showSnapshotButton: false,
+ showMoveButtons: [false, false],
+ showStreamAudioStreamButton: false,
+ showEditButton: false,
+ },
};
diff --git a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx
index bca9e709d2..00304c7b13 100644
--- a/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx
+++ b/packages/shared-components/src/room-list/RoomListHeaderView/RoomListHeaderView.stories.tsx
@@ -8,18 +8,19 @@
import React, { type JSX } from "react";
import { fn } from "storybook/test";
-import type { Meta, StoryFn } from "@storybook/react-vite";
+import type { Meta, StoryObj } from "@storybook/react-vite";
import {
RoomListHeaderView,
type RoomListHeaderViewActions,
type RoomListHeaderViewSnapshot,
} from "./RoomListHeaderView";
import { useMockedViewModel } from "../../viewmodel";
+import { withViewDocs } from "../../../.storybook/withViewDocs";
import { defaultSnapshot } from "./default-snapshot";
type RoomListHeaderProps = RoomListHeaderViewSnapshot & RoomListHeaderViewActions;
-const RoomListHeaderViewWrapper = ({
+const RoomListHeaderViewWrapperImpl = ({
createChatRoom,
createRoom,
createVideoRoom,
@@ -44,8 +45,9 @@ const RoomListHeaderViewWrapper = ({
});
return ;
};
+const RoomListHeaderViewWrapper = withViewDocs(RoomListHeaderViewWrapperImpl, RoomListHeaderView);
-export default {
+const meta = {
title: "Room List/RoomListHeaderView",
component: RoomListHeaderViewWrapper,
tags: ["autodocs"],
@@ -67,18 +69,21 @@ export default {
url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel?node-id=2925-19173",
},
},
-} as Meta;
+} satisfies Meta;
-const Template: StoryFn = (args) => ;
+export default meta;
+type Story = StoryObj;
-export const Default = Template.bind({});
+export const Default: Story = {};
-export const NoSpaceMenu = Template.bind({});
-NoSpaceMenu.args = {
- displaySpaceMenu: false,
+export const NoSpaceMenu: Story = {
+ args: {
+ displaySpaceMenu: false,
+ },
};
-export const NoComposeMenu = Template.bind({});
-NoComposeMenu.args = {
- displayComposeMenu: false,
+export const NoComposeMenu: Story = {
+ args: {
+ displayComposeMenu: false,
+ },
};
diff --git a/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.stories.tsx b/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.stories.tsx
index 72b619275e..507bb80766 100644
--- a/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.stories.tsx
+++ b/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.stories.tsx
@@ -12,6 +12,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
import type { Room } from "./RoomListItemView";
import { RoomListItemView, type RoomListItemSnapshot, type RoomListItemActions } from "./RoomListItemView";
import { useMockedViewModel } from "../../viewmodel";
+import { withViewDocs } from "../../../.storybook/withViewDocs";
import { defaultSnapshot } from "./default-snapshot";
import { renderAvatar } from "../story-mocks";
@@ -26,7 +27,7 @@ type RoomListItemProps = RoomListItemSnapshot &
};
// Wrapper component that creates a mocked ViewModel
-const RoomListItemWrapper = ({
+const RoomListItemWrapperImpl = ({
onOpenRoom,
onMarkAsRead,
onMarkAsUnread,
@@ -67,6 +68,7 @@ const RoomListItemWrapper = ({
/>
);
};
+const RoomListItemWrapper = withViewDocs(RoomListItemWrapperImpl, RoomListItemView);
const meta = {
title: "Room List/RoomListItemView",
diff --git a/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.stories.tsx b/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.stories.tsx
index d862cc4295..ff9578f2fb 100644
--- a/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.stories.tsx
+++ b/packages/shared-components/src/room-list/RoomListSearchView/RoomListSearchView.stories.tsx
@@ -8,17 +8,18 @@
import React, { type JSX } from "react";
import { fn } from "storybook/test";
-import type { Meta, StoryFn } from "@storybook/react-vite";
+import type { Meta, StoryObj } from "@storybook/react-vite";
import {
RoomListSearchView,
type RoomListSearchViewActions,
type RoomListSearchViewSnapshot,
} from "./RoomListSearchView";
import { useMockedViewModel } from "../../viewmodel";
+import { withViewDocs } from "../../../.storybook/withViewDocs";
type RoomListSearchProps = RoomListSearchViewSnapshot & RoomListSearchViewActions;
-const RoomListSearchViewWrapper = ({
+const RoomListSearchViewWrapperImpl = ({
onSearchClick,
onDialPadClick,
onExploreClick,
@@ -31,8 +32,9 @@ const RoomListSearchViewWrapper = ({
});
return ;
};
+const RoomListSearchViewWrapper = withViewDocs(RoomListSearchViewWrapperImpl, RoomListSearchView);
-export default {
+const meta = {
title: "Room List/RoomListSearchView",
component: RoomListSearchViewWrapper,
tags: ["autodocs"],
@@ -50,25 +52,29 @@ export default {
url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel-2025?node-id=98-1979&t=vafb4zoYMNLRuAbh-4",
},
},
-} as Meta;
+} satisfies Meta;
-const Template: StoryFn = (args) => ;
+export default meta;
+type Story = StoryObj;
-export const Default = Template.bind({});
+export const Default: Story = {};
-export const WithDialPad = Template.bind({});
-WithDialPad.args = {
- displayDialButton: true,
+export const WithDialPad: Story = {
+ args: {
+ displayDialButton: true,
+ },
};
-export const WithoutExplore = Template.bind({});
-WithoutExplore.args = {
- displayExploreButton: false,
+export const WithoutExplore: Story = {
+ args: {
+ displayExploreButton: false,
+ },
};
-export const AllButtons = Template.bind({});
-AllButtons.args = {
- displayExploreButton: true,
- displayDialButton: true,
- searchShortcut: "⌘ K",
+export const AllButtons: Story = {
+ args: {
+ displayExploreButton: true,
+ displayDialButton: true,
+ searchShortcut: "⌘ K",
+ },
};
diff --git a/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx b/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx
index 8e183fff4a..05fb9af6cc 100644
--- a/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx
+++ b/packages/shared-components/src/room-list/RoomListView/RoomListView.stories.tsx
@@ -13,6 +13,7 @@ import type { Room } from "../RoomListItemView";
import type { FilterId } from "../RoomListPrimaryFilters";
import { RoomListView, type RoomListSnapshot, type RoomListViewActions } from "./RoomListView";
import { useMockedViewModel } from "../../viewmodel";
+import { withViewDocs } from "../../../.storybook/withViewDocs";
import {
renderAvatar,
createGetRoomItemViewModel,
@@ -26,7 +27,7 @@ type RoomListViewProps = RoomListSnapshot & RoomListViewActions & { renderAvatar
const mockFilterIds: FilterId[] = ["unread", "people", "rooms", "favourite"];
// Wrapper component that creates a mocked ViewModel
-const RoomListViewWrapper = ({
+const RoomListViewWrapperImpl = ({
onToggleFilter,
createChatRoom,
createRoom,
@@ -44,6 +45,7 @@ const RoomListViewWrapper = ({
});
return ;
};
+const RoomListViewWrapper = withViewDocs(RoomListViewWrapperImpl, RoomListView);
const meta = {
title: "Room List/RoomListView",
diff --git a/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx
index aa12524a97..ff2d7a3cd6 100644
--- a/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx
+++ b/packages/shared-components/src/room-list/VirtualizedRoomListView/VirtualizedRoomListView.stories.tsx
@@ -13,6 +13,7 @@ import type { Room } from "../RoomListItemView";
import { VirtualizedRoomListView, type RoomListViewState } from "./VirtualizedRoomListView";
import type { RoomListSnapshot, RoomListViewActions } from "../RoomListView";
import { useMockedViewModel } from "../../viewmodel";
+import { withViewDocs } from "../../../.storybook/withViewDocs";
import type { FilterId } from "../RoomListPrimaryFilters";
import { renderAvatar, createGetRoomItemViewModel, mockRoomIds } from "../story-mocks";
@@ -22,7 +23,7 @@ type RoomListStoryProps = RoomListSnapshot & RoomListViewActions & { renderAvata
const storyRoomIds = mockRoomIds.slice(0, 10);
// Wrapper component that creates a mocked ViewModel
-const RoomListWrapper = ({
+const RoomListWrapperImpl = ({
onToggleFilter,
createChatRoom,
createRoom,
@@ -45,6 +46,7 @@ const RoomListWrapper = ({
);
};
+const RoomListWrapper = withViewDocs(RoomListWrapperImpl, VirtualizedRoomListView);
const mockFilterIds: FilterId[] = ["unread", "people"];
@@ -54,7 +56,7 @@ const defaultRoomListState: RoomListViewState = {
filterKeys: undefined,
};
-const meta: Meta = {
+const meta = {
title: "Room List/VirtualizedRoomListView",
component: RoomListWrapper,
tags: ["autodocs"],
@@ -86,9 +88,9 @@ const meta: Meta = {
),
],
-};
+} satisfies Meta;
export default meta;
-type Story = StoryObj;
+type Story = StoryObj;
export const Default: Story = {};
diff --git a/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.stories.tsx b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.stories.tsx
index 07b890139e..b0147afaec 100644
--- a/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.stories.tsx
+++ b/packages/shared-components/src/room/RoomStatusBar/RoomStatusBarView.stories.tsx
@@ -4,11 +4,12 @@
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
-import { type Meta, type StoryFn } from "@storybook/react-vite";
+import { type Meta, type StoryObj } from "@storybook/react-vite";
import React, { type JSX } from "react";
import { fn } from "storybook/test";
import { useMockedViewModel } from "../../viewmodel";
+import { withViewDocs } from "../../../.storybook/withViewDocs";
import {
RoomStatusBarState,
RoomStatusBarView,
@@ -18,7 +19,7 @@ import {
type RoomStatusBarProps = RoomStatusBarViewSnapshot & RoomStatusBarViewActions;
-const RoomStatusBarViewWrapper = ({
+const RoomStatusBarViewWrapperImpl = ({
onResendAllClick,
onDeleteAllClick,
onRetryRoomCreationClick,
@@ -33,8 +34,9 @@ const RoomStatusBarViewWrapper = ({
});
return ;
};
+const RoomStatusBarViewWrapper = withViewDocs(RoomStatusBarViewWrapperImpl, RoomStatusBarView);
-export default {
+const meta = {
title: "room/RoomStatusBarView",
component: RoomStatusBarViewWrapper,
tags: ["autodocs"],
@@ -45,61 +47,69 @@ export default {
onRetryRoomCreationClick: fn(),
onTermsAndConditionsClicked: fn(),
},
-} as Meta;
+} satisfies Meta;
-const Template: StoryFn = (args) => ;
+export default meta;
+type Story = StoryObj;
/**
* Rendered when the client has lost connection with the server.
*/
-export const WithConnectionLost = Template.bind({});
-WithConnectionLost.args = {
- state: RoomStatusBarState.ConnectionLost,
+export const WithConnectionLost: Story = {
+ args: {
+ state: RoomStatusBarState.ConnectionLost,
+ },
};
/**
* Rendered when the client needs the user to consent to some terms and conditions before
* they can perform any room actions.
*/
-export const WithConsentLink = Template.bind({});
-WithConsentLink.args = {
- state: RoomStatusBarState.NeedsConsent,
- consentUri: "#example",
+export const WithConsentLink: Story = {
+ args: {
+ state: RoomStatusBarState.NeedsConsent,
+ consentUri: "#example",
+ },
};
/**
* Rendered when the server has hit a usage limit and is forbidding the user from performing
* any actions in the room. There is an optional parameter to link to an admin to contact.
*/
-export const WithResourceLimit = Template.bind({});
-WithResourceLimit.args = {
- state: RoomStatusBarState.ResourceLimited,
- resourceLimit: "hs_disabled",
- adminContactHref: "#example",
+export const WithResourceLimit: Story = {
+ args: {
+ state: RoomStatusBarState.ResourceLimited,
+ resourceLimit: "hs_disabled",
+ adminContactHref: "#example",
+ },
};
/**
* Rendered when the client has some unsent messages in the room, stored locally.
*/
-export const WithUnsentMessages = Template.bind({});
-WithUnsentMessages.args = {
- state: RoomStatusBarState.UnsentMessages,
- isResending: false,
+export const WithUnsentMessages: Story = {
+ args: {
+ state: RoomStatusBarState.UnsentMessages,
+ isResending: false,
+ },
};
/**
* Rendered when the client has some unsent messages in the room, stored locally and is
* trying to send them.
*/
-export const WithUnsentMessagesSending = Template.bind({});
-WithUnsentMessagesSending.args = {
- state: RoomStatusBarState.UnsentMessages,
- isResending: true,
+export const WithUnsentMessagesSending: Story = {
+ args: {
+ state: RoomStatusBarState.UnsentMessages,
+ isResending: true,
+ },
};
+
/**
* Rendered when a local room has failed to be created.
*/
-export const WithLocalRoomRetry = Template.bind({});
-WithLocalRoomRetry.args = {
- state: RoomStatusBarState.LocalRoomFailed,
+export const WithLocalRoomRetry: Story = {
+ args: {
+ state: RoomStatusBarState.LocalRoomFailed,
+ },
};