diff --git a/examples/vite/src/App.tsx b/examples/vite/src/App.tsx
index 6942b7d431..90f480a7fd 100644
--- a/examples/vite/src/App.tsx
+++ b/examples/vite/src/App.tsx
@@ -159,7 +159,15 @@ const EmojiPickerWithCustomOptions = (
) => {
const { mode } = useAppSettingsSelector((state) => state.theme);
- return ;
+ return (
+
+ );
};
const ConfigurableNotificationList = (props: NotificationListProps) => {
diff --git a/examples/vite/src/index.scss b/examples/vite/src/index.scss
index 4fbce0e088..eae6820d30 100644
--- a/examples/vite/src/index.scss
+++ b/examples/vite/src/index.scss
@@ -1,9 +1,10 @@
-@layer stream-new, stream-overrides, stream-app-overrides;
+@layer stream-new, stream-new-plugins, stream-overrides, stream-app-overrides;
// v3 CSS import
@import url('stream-chat-react/dist/css/index.css') layer(stream-new);
@import url('./AppSettings/AppSettings.scss') layer(stream-app-overrides);
@import url('./SystemNotification/SystemNotification.scss') layer(stream-app-overrides);
+@import url('stream-chat-react/dist/css/emoji-picker.css') layer(stream-new-plugins);
:root {
font-synthesis: none;
diff --git a/package.json b/package.json
index 72b6b4443e..dac440d267 100644
--- a/package.json
+++ b/package.json
@@ -186,7 +186,7 @@
"scripts": {
"clean": "rm -rf dist",
"build": "yarn clean && concurrently 'yarn build-translations' 'vite build' 'tsc --project tsconfig.lib.json' 'yarn build-styling'",
- "build-styling": "sass src/styling/index.scss:dist/css/index.css src/styling/_emoji-replacement.scss:dist/css/emoji-replacement.css; cp -r src/styling/assets dist/css/assets",
+ "build-styling": "sass src/styling/index.scss:dist/css/index.css src/styling/_emoji-replacement.scss:dist/css/emoji-replacement.css src/plugins/Emojis/styling/index.scss:dist/css/emoji-picker.css; cp -r src/styling/assets dist/css/assets",
"build-translations": "i18next-cli extract",
"coverage": "vitest run --coverage",
"lint": "yarn prettier --list-different && yarn eslint && yarn validate-translations",
diff --git a/scripts/watch-styling.mjs b/scripts/watch-styling.mjs
index 9d429edb78..033a606db2 100644
--- a/scripts/watch-styling.mjs
+++ b/scripts/watch-styling.mjs
@@ -6,8 +6,20 @@ import { fileURLToPath } from 'node:url';
import { compileAsync } from 'sass';
const SRC_DIR = path.resolve('src');
-const ENTRY_FILE = path.join(SRC_DIR, 'styling/index.scss');
-const OUTPUT_FILE = path.resolve('dist/css/index.css');
+const STYLE_ENTRYPOINTS = [
+ {
+ entryFile: path.join(SRC_DIR, 'styling/index.scss'),
+ outputFile: path.resolve('dist/css/index.css'),
+ },
+ {
+ entryFile: path.join(SRC_DIR, 'styling/_emoji-replacement.scss'),
+ outputFile: path.resolve('dist/css/emoji-replacement.css'),
+ },
+ {
+ entryFile: path.join(SRC_DIR, 'plugins/Emojis/styling/index.scss'),
+ outputFile: path.resolve('dist/css/emoji-picker.css'),
+ },
+];
const SCSS_EXTENSION = '.scss';
const BUILD_DELAY_MS = 150;
const SCAN_INTERVAL_MS = 500;
@@ -29,9 +41,9 @@ const log = (message) => {
const isScssFile = (filename) => filename.endsWith(SCSS_EXTENSION);
-const toOutputRelativePath = (source) =>
+const toOutputRelativePath = (source, outputFile) =>
path
- .relative(path.dirname(OUTPUT_FILE), fileURLToPath(source))
+ .relative(path.dirname(outputFile), fileURLToPath(source))
.split(path.sep)
.join('/');
@@ -86,23 +98,29 @@ const flushQueuedBuild = () => {
void runBuild(trigger);
};
-const buildStyling = async () => {
- const { css, sourceMap } = await compileAsync(ENTRY_FILE, {
+const buildStyleEntry = async ({ entryFile, outputFile }) => {
+ const { css, sourceMap } = await compileAsync(entryFile, {
sourceMap: true,
style: 'expanded',
});
- const sourceMapFile = `${path.basename(OUTPUT_FILE)}.map`;
+ const sourceMapFile = `${path.basename(outputFile)}.map`;
const normalizedSourceMap = {
...sourceMap,
- file: path.basename(OUTPUT_FILE),
+ file: path.basename(outputFile),
sources: sourceMap.sources.map((source) =>
- source.startsWith('file://') ? toOutputRelativePath(source) : source,
+ source.startsWith('file://') ? toOutputRelativePath(source, outputFile) : source,
),
};
- await mkdir(path.dirname(OUTPUT_FILE), { recursive: true });
- await writeFile(OUTPUT_FILE, `${css}\n\n/*# sourceMappingURL=${sourceMapFile} */\n`);
- await writeFile(`${OUTPUT_FILE}.map`, JSON.stringify(normalizedSourceMap));
+ await mkdir(path.dirname(outputFile), { recursive: true });
+ await writeFile(outputFile, `${css}\n\n/*# sourceMappingURL=${sourceMapFile} */\n`);
+ await writeFile(`${outputFile}.map`, JSON.stringify(normalizedSourceMap));
+};
+
+const buildStyling = async () => {
+ for (const entry of STYLE_ENTRYPOINTS) {
+ await buildStyleEntry(entry);
+ }
};
const runBuild = async (trigger) => {
diff --git a/src/plugins/Emojis/EmojiPicker.tsx b/src/plugins/Emojis/EmojiPicker.tsx
index 3e60cb3b38..50d6710e84 100644
--- a/src/plugins/Emojis/EmojiPicker.tsx
+++ b/src/plugins/Emojis/EmojiPicker.tsx
@@ -64,6 +64,7 @@ export const EmojiPicker = (props: EmojiPickerProps) => {
const { pickerContainerClassName, wrapperClassName } = classNames;
const { ButtonIconComponent = IconEmoji } = props;
+ const pickerStyle = props.pickerProps?.style as React.CSSProperties | undefined;
useEffect(() => {
if (!popperElement || !referenceElement) return;
@@ -107,6 +108,7 @@ export const EmojiPicker = (props: EmojiPickerProps) => {
}
}}
{...props.pickerProps}
+ style={{ ...pickerStyle, '--shadow': 'none' }}
/>
)}
diff --git a/src/plugins/Emojis/styling/EmojiPicker.scss b/src/plugins/Emojis/styling/EmojiPicker.scss
new file mode 100644
index 0000000000..5cf3cba2ef
--- /dev/null
+++ b/src/plugins/Emojis/styling/EmojiPicker.scss
@@ -0,0 +1,11 @@
+.str-chat__message-textarea-emoji-picker-container {
+ --str-chat__emoji-picker-border-radius: 10px;
+
+ border-radius: var(--str-chat__emoji-picker-border-radius);
+ box-shadow: var(--str-chat__box-shadow-3);
+ overflow: hidden;
+
+ em-emoji-picker {
+ --border-radius: var(--str-chat__emoji-picker-border-radius);
+ }
+}
diff --git a/src/plugins/Emojis/styling/index.scss b/src/plugins/Emojis/styling/index.scss
new file mode 100644
index 0000000000..8ccd11357c
--- /dev/null
+++ b/src/plugins/Emojis/styling/index.scss
@@ -0,0 +1 @@
+@use 'EmojiPicker';
diff --git a/src/styling/index.scss b/src/styling/index.scss
index e0bb678f76..73da666698 100644
--- a/src/styling/index.scss
+++ b/src/styling/index.scss
@@ -5,7 +5,7 @@
@use 'global-theme-variables';
@use 'palette-variables';
@use './variables-tokens.css';
-@use 'variables/font';
+@use 'variables';
@use 'base';
@use 'fonts';
diff --git a/src/styling/variables/index.scss b/src/styling/variables/index.scss
new file mode 100644
index 0000000000..a557be7083
--- /dev/null
+++ b/src/styling/variables/index.scss
@@ -0,0 +1,2 @@
+@use 'font';
+@use 'shadows';
diff --git a/src/styling/variables/shadows.scss b/src/styling/variables/shadows.scss
new file mode 100644
index 0000000000..2957d4d628
--- /dev/null
+++ b/src/styling/variables/shadows.scss
@@ -0,0 +1,30 @@
+/*
+Shadows on Web communicate visual separation and hierarchy through composed box-shadow layers.
+
+Unlike iOS (single shadow token) and Android (dp elevation), web elevation is constructed
+from multiple shadow layers to simulate depth and ambient light.
+
+Each shadow level consists of predefined layered shadows.
+Components must use these tokens instead of defining custom box-shadow values.
+
+Higher levels combine stronger blur, increased offset, and additional ambient layers
+to create clearer separation from the base surface.
+ */
+
+.str-chat,
+.str-chat__theme-light {
+ --str-chat__box-shadow-1: var(--light-elevation-1);
+ --str-chat__box-shadow-2: var(--light-elevation-2);
+ --str-chat__box-shadow-3: var(--light-elevation-3);
+ --str-chat__box-shadow-4: var(--light-elevation-4);
+}
+
+.str-chat__theme-dark,
+.str-chat:not(.str-chat__theme-dark)
+ *:not(.str-chat__theme-dark)
+ .str-chat__theme-inverse {
+ --str-chat__box-shadow-1: var(--dark-elevation-1);
+ --str-chat__box-shadow-2: var(--dark-elevation-2);
+ --str-chat__box-shadow-3: var(--dark-elevation-3);
+ --str-chat__box-shadow-4: var(--dark-elevation-4);
+}