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); +}