* Fetches link previews for all links in message (instead of just the first one) * message url component to display preview for multiple urls * Revert visible behaviour back to only showing one URL preview While fetching all link previews in the text message. * Moved URL preview VM to MessageComposer * Added com.beeper.linkpreviews to messages sent with the markdown compositor * MSC 4452 implemented for the older (markdown) composer * claude told me to use logger instead of console * Previews generated with snapshot instead of vm * moved attachPreviews to a separate file * added attach URL previews to rich text editor * don't let attachUrlPreviews block clearning the composer * fixed linter errors * moved url preview behind labs feature gate * passed linters * moved lab feature checking to where attach preview is used * claude wrote some unit tests for url previewing * fixed linter errors * added feature to labs.md * fixed linter errors * fixed oxfmt error * set previews to none if all previews failed * resolved PR reviews besides the ones that requires a larger code change * moved url preview VM creation to message composer * removed delay when clearing composer url preview on message send * Collpased URL previews in timeline * CSS for collapsed URL previews * fixed typo thing? * update class for this component * remove dead code from css * minor changes to stop using globals everywhere * moved debouncing responsibility to urlpreviewVM * minor lint fixes * added comment * remove composer content from the state of urlpreview, moved it to the vm * urlpreviewwrapper depends on the vm only * added comments * edited the comment * one slow preview no longer blocks up the current batch of previews from loading * show failed and loading url previews * collapsed urlpreview styles to match the expanded preview * css formtting * composer url preview summary bar * prefer site icon * Revert "Merge branch 'url-preview-loading-indicator' into collapsed-url-previews" This reverts commit b7a1ccb0e74661458e16c128814ba1939bdf400e, reversing changes made to 1340b216407cf9af95741174819f97a144c13bb3. * updated shared-components Punit test snapshots * updated snapshots * wait for tall image to load before capturing snapshot (claude wrote this) * try remove requestanimation frame * use semantic tokens instead of specifying hard values in some places * Update packages/shared-components/src/room/timeline/event-tile/UrlPreviewGroupView/LinkPreview/LinkPreview.tsx Co-authored-by: Florian Duros <florian.duros@ormaz.fr> * changed css so previews looks less horrendous on different front sizes, particularly very small font sizes * updated unit test snapshots * regenerated playwright story screenshots --------- Co-authored-by: Florian Duros <florian.duros@ormaz.fr>
@element-hq/web-shared-components
Shared React components library for Element Web, Aurora, Element modules... This package provides opinionated UI components built on top of the Compound Design System and Compound Web. This is not a design system by itself, but rather a set of larger components.
Installation in a new project
When adding this library to a new project, as well as installing
@element-hq/web-shared-components as normal, you will also need to add
compound-web as a peer
dependency:
pnpm add @element-hq/web-shared-components
pnpm add @vector-im/compound-web
(This avoids problems where we end up with different versions of compound-web in the top-level project and web-shared-components).
Usage
Basic Import
Both JavaScript and CSS can be imported as follows:
import { RoomListHeaderView, useViewModel } from "@element-hq/web-shared-components";
import "@element-hq/web-shared-components/dist/element-web-shared-components.css";
or in CSS file:
@import url("@element-hq/web-shared-components");
Sub-path Imports
Callers running outside the browser DOM (e.g. inside an AudioWorkletGlobalScope
or a worker) can pull in the small standalone numbers utility bundle without
loading the rest of the package bundle, which transitively imports React,
dnd-kit, and other code that touches window / document:
import { percentageOf, percentageWithin } from "@element-hq/web-shared-components/numbers";
The sub-path exposes the same functions listed under Formatting
and ships as its own ES/CJS bundle in dist/numbers.{js,umd.cjs}. Prefer the
main package entry for everything else.
Using Components
There are two kinds of components in this library:
- regular react component which doesn't follow specific pattern.
- view component(MVVM pattern).
Tip
These components are available in the project storybook.
Regular Components
These components can be used directly by passing props. Example:
import { Flex } from "@element-hq/web-shared-components";
function MyApp() {
return <Flex align="center" />;
}
View (MVVM) Components
These components follow the MVVM pattern. A ViewModel instance should be provided as a prop.
Here's a basic example:
import { ViewExample } from "@element-hq/web-shared-components";
function MyApp() {
const viewModel = new ViewModelExample();
return <ViewExample vm={viewModel} />;
}
Utilities
Internationalization
useI18n()- Hook for translationsI18nApi- Internationalization API utilities
Date & Time
DateUtils- Date formatting and manipulationhumanize- Human-readable time formatting
Formatting
FormattingUtils- Text and data formatting utilitiesnumbers- Number formatting utilities
Development
Prerequisites
- Node.js >= 20.0.0
- pnpm => 10
Setup
# Install dependencies
pnpm install
# Build the library
pnpm prepack
Running Storybook
pnpm storybook
Write components
Most components should be written as MVVM pattern view components. See existing components for examples. The exceptions are low level components that don't need a view model.
Write Storybook Stories
All components should have accompanying Storybook stories for documentation and visual testing. Stories are written in TypeScript using the Component Story Format (CSF).
Use shallow, browse-oriented story titles such as RoomList/RoomListSearchView or TimelineBody/DecryptionFailureBodyView. Do not mirror the full source path in the Storybook title.
Story File Structure
Place the story file next to the component with the .stories.tsx extension:
MyComponent/
├── MyComponent.tsx
├── MyComponent.module.css
└── MyComponent.stories.tsx
Regular Component Stories
For regular React components (non-MVVM), create stories by defining a meta object and story variations:
import type { Meta, StoryObj } from "@storybook/react-vite";
import { fn } from "storybook/test";
import { MyComponent } from "./MyComponent";
const meta = {
title: "Category/MyComponent",
component: MyComponent,
tags: ["autodocs"],
args: {
// Default args for all stories
label: "Default Label",
onClick: fn(), // Mock function for tracking interactions
},
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
// Default story uses the default args
export const Default: Story = {};
// Override specific args for variations
export const WithCustomLabel: Story = {
args: {
label: "Custom Label",
},
};
export const Disabled: Story = {
args: {
disabled: true,
},
};
MVVM Component Stories
For MVVM components, create a wrapper component that uses useMockedViewModel and withViewDocs:
import React, { type JSX } from "react";
import { fn } from "storybook/test";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { MyComponentView, type MyComponentViewSnapshot, type MyComponentViewActions } from "./MyComponentView";
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.
// 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 <MyComponentView vm={vm} />;
};
// withViewDocs copies the View's JSDoc description onto the wrapper for Storybook autodocs
const MyComponentViewWrapper = withViewDocs(MyComponentViewWrapperImpl, MyComponentView);
// Must use `satisfies` (not `as` or `: Meta`) to preserve type info for docgen
const meta = {
title: "Category/MyComponentView",
component: MyComponentViewWrapper,
tags: ["autodocs"],
args: {
// Snapshot properties (state)
title: "Default Title",
isLoading: false,
// Action properties (callbacks)
onAction: fn(),
},
} satisfies Meta<typeof MyComponentViewWrapper>;
export default meta;
type Story = StoryObj<typeof MyComponentViewWrapper>;
export const Default: Story = {};
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:
- Named wrapper variable — the wrapper must be assigned to a named
const(e.g.MyComponentViewWrapperImpl) before being passed towithViewDocs, so thatreact-docgen-typescriptcan extract its props.withViewDocscall — wraps the wrapper component with the original View to copy the View's JSDoc description.satisfies Meta— the meta object must usesatisfies Meta<...>(notas 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 to embed Figma designs directly in Storybook. This helps developers compare their implementation with the design specs.
- Get the Figma URL: Open your design in Figma, click "Share" → "Copy link"
- Add to story parameters: Include the
designobject in the meta'sparameters - Supported URL formats:
- File links:
https://www.figma.com/file/... - Design links:
https://www.figma.com/design/... - Specific node:
https://www.figma.com/design/...?node-id=123-456
- File links:
Example with Figma integration:
const meta = {
title: "RoomList/RoomListSearchView",
component: RoomListSearchViewWrapper,
tags: ["autodocs"],
args: {
// ... your args
},
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel?node-id=98-1979",
},
},
} satisfies Meta<typeof RoomListSearchViewWrapper>;
export default meta;
The Figma design will appear in the "Design" tab in Storybook.
Non-UI Utility Stories
For utility functions, helpers, and other non-UI exports, create documentation stories using TSX format with TypeDoc-generated markdown.
src/core/utils/humanize.stories.tsx
import React from "react";
import { Markdown } from "@storybook/addon-docs/blocks";
import type { Meta } from "@storybook/react-vite";
import humanizeTimeDoc from "../../typedoc/functions/humanizeTime.md?raw";
const meta = {
title: "Core/Humanize",
parameters: {
docs: {
page: () => (
<>
<h1>humanize</h1>
<Markdown>{humanizeTimeDoc}</Markdown>
</>
),
},
},
tags: ["autodocs", "skip-test"],
} satisfies Meta;
export default meta;
// Docs-only story - renders nothing but triggers autodocs
export const Docs = {
render: () => null,
};
Note
Be sure to include the
skip-testtag in your utility stories to prevent them from running as visual tests.
Workflow:
- Write TsDoc in your utility function
- Export the function from
src/index.ts - Run
pnpm build:docto generate TypeDoc markdown - Create a
.stories.tsxfile importing the generated markdown - The documentation appears automatically in Storybook
Tests
Two types of tests are available: unit tests and visual regression tests.
Unit Tests
These tests cover the logic of the components and utilities. Built with Vitest and React Testing Library.
pnpm test:unit
Visual Regression Tests
These tests ensure the UI components render correctly. Built with Storybook and run under vitest using playwright.
pnpm test:storybook:update
Each story will be rendered and a screenshot will be taken and compared to the existing baseline. If there are visual changes or AXE violation, the test will fail.
Screenshots are located in packages/shared-components/__vis__/.
Important
In case of docker issues with Playwright, see playwright EW documentation.
Translations
First see our translation guide and translation dev guide. To generate translation strings for this package, run:
pnpm i18n
Publish a new version
Two steps are required to publish a new version of this package:
- Bump the version in
package.jsonfollowing semver rules and open a PR. - Once merged run the github workflow