diff --git a/.changelog/5997-fix-css-fluent-context-2.md b/.changelog/5997-fix-css-fluent-context-2.md new file mode 100644 index 0000000000..f44635dce9 --- /dev/null +++ b/.changelog/5997-fix-css-fluent-context-2.md @@ -0,0 +1,7 @@ +### Changed + +- Converted the tooltip, modal dialog, file attachment and transcript root classes to CSS Modules, keeping their `webchat__` class names, so Fluent styles them through the CSS module theme, in PR [#5997](https://github.com/microsoft/BotFramework-WebChat/pull/5997), by [@OEvgeny](https://github.com/OEvgeny) + +### Removed + +- 💥 Deprecated the `modalDialog` and `tooltip` style set entries, which no longer affect rendering, in PR [#5997](https://github.com/microsoft/BotFramework-WebChat/pull/5997), by [@OEvgeny](https://github.com/OEvgeny) diff --git a/__tests__/html2/fluentTheme/modalDialog.closeButton.hover.dark.html b/__tests__/html2/fluentTheme/modalDialog.closeButton.hover.dark.html new file mode 100644 index 0000000000..d5a873a30d --- /dev/null +++ b/__tests__/html2/fluentTheme/modalDialog.closeButton.hover.dark.html @@ -0,0 +1,112 @@ + + + + Fluent Theme (dark): focused modal dialog close button on hover + + + + + + + + + + +
+ + + diff --git a/__tests__/html2/fluentTheme/modalDialog.closeButton.hover.dark.html.snap-1.png b/__tests__/html2/fluentTheme/modalDialog.closeButton.hover.dark.html.snap-1.png new file mode 100644 index 0000000000..b65a8745c2 Binary files /dev/null and b/__tests__/html2/fluentTheme/modalDialog.closeButton.hover.dark.html.snap-1.png differ diff --git a/__tests__/html2/fluentTheme/modalDialog.closeButton.hover.dark.html.snap-2.png b/__tests__/html2/fluentTheme/modalDialog.closeButton.hover.dark.html.snap-2.png new file mode 100644 index 0000000000..c3c36c8096 Binary files /dev/null and b/__tests__/html2/fluentTheme/modalDialog.closeButton.hover.dark.html.snap-2.png differ diff --git a/packages/component/src/Attachment/FileContent.module.css b/packages/component/src/Attachment/FileContent.module.css new file mode 100644 index 0000000000..7022209892 --- /dev/null +++ b/packages/component/src/Attachment/FileContent.module.css @@ -0,0 +1,21 @@ +/* Style options apply through the "fileContent" style set entry, which selects the "webchat__fileContent" class names. */ +:global(.webchat) .fileContent { + display: flex; + + .fileContent__buttonLink { + display: flex; + flex: 1; + } + + .fileContent__badge { + display: flex; + flex: 1; + flex-direction: column; + } + + .fileContent__downloadIcon, + .fileContent__fileName, + .fileContent__size { + --webchat-preserve-reason: target-class; + } +} diff --git a/packages/component/src/Attachment/FileContent.tsx b/packages/component/src/Attachment/FileContent.tsx index 9fefdb737a..a9443c9dc8 100644 --- a/packages/component/src/Attachment/FileContent.tsx +++ b/packages/component/src/Attachment/FileContent.tsx @@ -1,29 +1,16 @@ import { validateProps } from '@msinternal/botframework-webchat-react-valibot'; +import { useStyles, webChatPrefixedClassName } from '@msinternal/botframework-webchat-styles/react'; import { hooks } from 'botframework-webchat-api'; -import classNames from 'classnames'; -import React, { memo } from 'react'; +import cx from 'classnames'; +import React, { Fragment, memo } from 'react'; import { boolean, number, object, optional, pipe, readonly, string, type InferInput } from 'valibot'; -import { useStyleToEmotionObject } from '../hooks/internal/styleToEmotionObject'; import useStyleSet from '../hooks/useStyleSet'; import { ComponentIcon } from '../Icon'; -const { useByteFormatter, useDirection, useLocalizer } = hooks; - -const ROOT_STYLE = { - display: 'flex', - - '& .webchat__fileContent__buttonLink': { - display: 'flex', - flex: 1 - }, +import styles from './FileContent.module.css'; - '& .webchat__fileContent__badge': { - display: 'flex', - flex: 1, - flexDirection: 'column' - } -}; +const { useByteFormatter, useDirection, useLocalizer } = hooks; // TODO: Consider using `useSanitizeHrefCallback`, which underlying use `sanitize-html` or whatever in HTML content transformer. const ALLOWED_PROTOCOLS = ['blob:', 'data:', 'http:', 'https:']; @@ -50,28 +37,31 @@ type FileContentBadgeProps = InferInput; const FileContentBadge = (props: FileContentBadgeProps) => { const { downloadIcon = false, fileName, size } = validateProps(fileContentBadgePropsSchema, props); + // Keeps the "webchat__fileContent" class names. + const classNames = useStyles(styles, webChatPrefixedClassName); const [direction] = useDirection(); const formatByte = useByteFormatter(); const localizedSize = typeof size === 'number' && formatByte(size); return ( - -
-
{fileName}
- {!!localizedSize &&
{localizedSize}
} + +
+
{fileName}
+ {!!localizedSize &&
{localizedSize}
}
{downloadIcon && ( )} - +
); }; @@ -90,10 +80,11 @@ const fileContentPropsSchema = pipe( function FileContent(props: FileContentProps) { const { className, href, fileName, size } = validateProps(fileContentPropsSchema, props); + // Keeps the "webchat__fileContent" class names. + const classNames = useStyles(styles, webChatPrefixedClassName); const [{ fileContent: fileContentStyleSet }] = useStyleSet(); const localize = useLocalizer(); const localizeBytes = useByteFormatter(); - const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + ''; const localizedSize = typeof size === 'number' && localizeBytes(size); @@ -112,13 +103,13 @@ function FileContent(props: FileContentProps) { ); return ( -
+
{sanitizedHref ? ( // URL is sanitized. // eslint-disable-next-line react/forbid-elements { ), { + 'aria-label': localize('ACTIVITY_CODE_ALT', title ?? ''), className: classNames['view-code-dialog'], - 'aria-label': localize('ACTIVITY_CODE_ALT', title ?? '') + cssModuleTheme: themeStyles } ); }, [code, isAIGenerated, language, localize, showModal, title, classNames]); diff --git a/packages/component/src/Attachment/Text/private/ViewCodeDialog.module.css b/packages/component/src/Attachment/Text/private/ViewCodeDialog.module.css index 10dc6c50e4..e5b268b7d2 100644 --- a/packages/component/src/Attachment/Text/private/ViewCodeDialog.module.css +++ b/packages/component/src/Attachment/Text/private/ViewCodeDialog.module.css @@ -1,33 +1,3 @@ -:global(.webchat) .view-code-dialog { - box-sizing: border-box; - display: grid; - height: 100%; - max-height: 100vh; - overflow: hidden; - padding: 1rem; - place-items: center; -} - -:global(.webchat) .view-code-dialog :global(.webchat__modal-dialog__box) { - display: flex; - flex-direction: column; - max-height: 100%; - max-width: 100%; - position: relative; -} - -:global(.webchat) .view-code-dialog :global(.webchat__modal-dialog__close-button-layout) { - position: absolute; - right: 0; -} - -:global(.webchat) .view-code-dialog :global(.webchat__modal-dialog__body) { - display: flex; - flex-direction: column; - gap: var(--webchat__padding--regular); - overflow: hidden; -} - :global(.webchat) .view-code-dialog__header { align-items: center; display: flex; diff --git a/packages/component/src/Attachment/Text/private/ViewCodeDialogTheme.module.css b/packages/component/src/Attachment/Text/private/ViewCodeDialogTheme.module.css new file mode 100644 index 0000000000..3a8c1104b4 --- /dev/null +++ b/packages/component/src/Attachment/Text/private/ViewCodeDialogTheme.module.css @@ -0,0 +1,32 @@ +/* The CSS module theme of the view code dialog: ActivityViewCodeButton passes it whole to showModal(), so the modal + dialog's parts get these class names, and only in this dialog. */ +/* The modal dialog rules (Popover.module.css) can come later in the stylesheet: ":is(dialog)" out-specifies them. */ +:global(.webchat) .modal-dialog:is(dialog) { + box-sizing: border-box; + display: grid; + height: 100%; + max-height: 100vh; + overflow: hidden; + padding: 1rem; + place-items: center; + + .modal-dialog__box { + display: flex; + flex-direction: column; + max-height: 100%; + max-width: 100%; + position: relative; + } + + .modal-dialog__close-button-layout { + position: absolute; + right: 0; + } + + .modal-dialog__body { + display: flex; + flex-direction: column; + gap: var(--webchat__padding--regular); + overflow: hidden; + } +} diff --git a/packages/component/src/BasicTranscript.module.css b/packages/component/src/BasicTranscript.module.css new file mode 100644 index 0000000000..009f0a845b --- /dev/null +++ b/packages/component/src/BasicTranscript.module.css @@ -0,0 +1,32 @@ +:global(.webchat) .basic-transcript { + /* The element is also the transcript focus area, which TranscriptFocus.module.css lays out as a grid. */ + overflow: hidden; + /* Make sure to set "position: relative" here to form another stacking context for the scroll-to-end button. */ + /* Stacking context help isolating elements that use "z-index" from global pollution. */ + /* https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context */ + position: relative; + + .basic-transcript__filler { + flex: 1; + } + + /* ActivityRow: no styles of its own, themes style it. */ + .basic-transcript__activity-body { + --webchat-preserve-reason: target-class; + } + + .basic-transcript__scrollable { + display: flex; + flex-direction: column; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; + } + + .basic-transcript__transcript { + list-style-type: none; + } + + .basic-transcript__activity-focus-target { + pointer-events: none; + } +} diff --git a/packages/component/src/BasicTranscript.tsx b/packages/component/src/BasicTranscript.tsx index 763cc6ab73..7c4374ecea 100644 --- a/packages/component/src/BasicTranscript.tsx +++ b/packages/component/src/BasicTranscript.tsx @@ -1,6 +1,7 @@ // TODO: [P2] Fix ESLint error `no-use-before-define` /* eslint-disable @typescript-eslint/no-use-before-define */ +import { useStyles, webChatPrefixedClassName } from '@msinternal/botframework-webchat-styles/react'; import { hooks } from 'botframework-webchat-api'; import classNames from 'classnames'; import React, { @@ -37,7 +38,6 @@ import { type ActivityElementMap } from './Transcript/types'; import FocusRedirector from './Utils/FocusRedirector'; import inputtableKey from './Utils/TypeFocusSink/inputtableKey'; import { android } from './Utils/detectBrowser'; -import { useStyleToEmotionObject } from './hooks/internal/styleToEmotionObject'; import useDispatchScrollPosition from './hooks/internal/useDispatchScrollPosition'; import useDispatchTranscriptFocusByActivityKey from './hooks/internal/useDispatchTranscriptFocusByActivityKey'; import useNonce from './hooks/internal/useNonce'; @@ -65,6 +65,8 @@ import useFocusRelativeActivity from './providers/TranscriptFocus/useFocusRelati import useFocusedExplicitly from './providers/TranscriptFocus/useFocusedExplicitly'; import useFocusedKey from './providers/TranscriptFocus/useFocusedKey'; +import styles from './BasicTranscript.module.css'; + const { useActivityKeys, useDirection, @@ -77,37 +79,6 @@ const { useStyleOptions } = hooks; -const ROOT_STYLE = { - '&.webchat__basic-transcript': { - display: 'flex', - flexDirection: 'column', - overflow: 'hidden', - // Make sure to set "position: relative" here to form another stacking context for the scroll-to-end button. - // Stacking context help isolating elements that use "z-index" from global pollution. - // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context - position: 'relative', - - '& .webchat__basic-transcript__filler': { - flex: 1 - }, - - '& .webchat__basic-transcript__scrollable': { - display: 'flex', - flexDirection: 'column', - overflowX: 'hidden', - WebkitOverflowScrolling: 'touch' - }, - - '& .webchat__basic-transcript__transcript': { - listStyleType: 'none' - }, - - '& .webchat__basic-transcript__activity-focus-target': { - pointerEvents: 'none' - } - } -}; - type ScrollBehavior = 'auto' | 'smooth'; type ScrollToOptions = { behavior?: ScrollBehavior }; type ScrollToPosition = { activityID?: string; scrollTop?: number }; @@ -135,8 +106,9 @@ const InternalTranscript = memo( const getActivityByKey = useGetActivityByKey(); const getKeyByActivityId = useGetKeyByActivityId(); const localize = useLocalizer(); - const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + ''; const rootElementRef = useRef(null); + // Keeps the "webchat__basic-transcript" class names. + const transcriptClassNames = useStyles(styles, webChatPrefixedClassName); const focusedKeyRef = useValueRef(focusedKey); const transcriptAriaLabel = localize('TRANSCRIPT_ARIA_LABEL_ALT'); @@ -444,7 +416,7 @@ const InternalTranscript = memo( // We are disabling `aria-activedescendant` for Android. See for details. aria-activedescendant={android ? undefined : activeDescendantId} aria-label={transcriptAriaLabel} - className={classNames('webchat__basic-transcript', rootClassName, (className || '') + '')} + className={classNames(transcriptClassNames['basic-transcript'], (className || '') + '')} dir={direction} onFocus={handleFocus} onKeyDown={handleTranscriptKeyDown} @@ -488,6 +460,8 @@ const InternalTranscriptScrollable = ({ children, onFocusFiller }: InternalTrans const [sticky]: [boolean] = useSticky(); const localize = useLocalizer(); const markAllAsAcknowledged = useMarkAllAsAcknowledged(); + // Keeps the "webchat__basic-transcript" class names. + const transcriptClassNames = useStyles(styles, webChatPrefixedClassName); const prevSticky = usePrevious(sticky); const transcriptRoleDescription = localize('TRANSCRIPT_ARIA_ROLE_ALT'); @@ -505,12 +479,12 @@ const InternalTranscriptScrollable = ({ children, onFocusFiller }: InternalTrans return ( - -
+ +
{hasAnyChild && ( {children} diff --git a/packages/component/src/BasicWebChat.module.css b/packages/component/src/BasicWebChat.module.css new file mode 100644 index 0000000000..827d905653 --- /dev/null +++ b/packages/component/src/BasicWebChat.module.css @@ -0,0 +1,4 @@ +:global(.webchat) .surface { + display: flex; + flex-direction: column; +} diff --git a/packages/component/src/BasicWebChat.tsx b/packages/component/src/BasicWebChat.tsx index f910c43d18..1a2411f06c 100644 --- a/packages/component/src/BasicWebChat.tsx +++ b/packages/component/src/BasicWebChat.tsx @@ -2,6 +2,7 @@ /* eslint react/no-unsafe: off */ import { validateProps } from '@msinternal/botframework-webchat-react-valibot'; +import { useStyles, webChatPrefixedClassName } from '@msinternal/botframework-webchat-styles/react'; import { SendBoxMiddlewareProxy, hooks } from 'botframework-webchat-api'; import classNames from 'classnames'; import React, { memo, useRef } from 'react'; @@ -14,12 +15,9 @@ import { useStyleToEmotionObject } from './hooks/internal/styleToEmotionObject'; import useStyleSet from './hooks/useStyleSet'; import AccessKeySinkSurface from './Utils/AccessKeySink/Surface'; -const { useStyleOptions } = hooks; +import styles from './BasicWebChat.module.css'; -const ROOT_STYLE = { - display: 'flex', - flexDirection: 'column' -}; +const { useStyleOptions } = hooks; const CONNECTIVITY_STATUS_STYLE = { flexShrink: 0 @@ -53,19 +51,20 @@ function BasicWebChat(props: BasicWebChatProps) { const { className, role } = validateProps(basicWebChatPropsSchema, props, 'strict'); const rootRef = useRef(null); + // Keeps the "webchat__surface" class name. + const basicWebChatClassNames = useStyles(styles, webChatPrefixedClassName); const [{ root: rootStyleSet }] = useStyleSet(); const [options] = useStyleOptions(); const styleToEmotionObject = useStyleToEmotionObject(); const connectivityStatusClassName = styleToEmotionObject(CONNECTIVITY_STATUS_STYLE) + ''; - const rootClassName = styleToEmotionObject(ROOT_STYLE) + ''; const sendBoxClassName = styleToEmotionObject(SEND_BOX_CSS) + ''; const toasterClassName = styleToEmotionObject(TOASTER_STYLE) + ''; const transcriptClassName = styleToEmotionObject(TRANSCRIPT_STYLE) + ''; return ( diff --git a/packages/component/src/Composer.module.css b/packages/component/src/Composer.module.css new file mode 100644 index 0000000000..7dff6c19be --- /dev/null +++ b/packages/component/src/Composer.module.css @@ -0,0 +1,11 @@ +:global(.webchat).css-custom-properties { + :global(.webchat__live-region) { + color: transparent; + height: 1px; + overflow: hidden; + position: absolute; + top: 0; + white-space: nowrap; + width: 1px; + } +} diff --git a/packages/component/src/Composer.tsx b/packages/component/src/Composer.tsx index 789ae10e19..4f34cef390 100644 --- a/packages/component/src/Composer.tsx +++ b/packages/component/src/Composer.tsx @@ -2,6 +2,7 @@ import { singleToArray } from '@msinternal/botframework-webchat-base/utils'; import { useMemoIterable } from '@msinternal/botframework-webchat-react-hooks'; +import { useStyles, webChatPrefixedClassName } from '@msinternal/botframework-webchat-styles/react'; import { Composer as APIComposer, AttachmentForScreenReaderMiddleware, @@ -67,6 +68,8 @@ import { type FocusTranscriptInit } from './types/internal/FocusTranscriptInit'; import downscaleImageToDataURL from './Utils/downscaleImageToDataURL'; import mapMap from './Utils/mapMap'; +import styles from './Composer.module.css'; + const { useGetActivityByKey, useReferenceGrammarID, useStyleOptions, useTrackException } = hooks; function styleSetToEmotionObjects(styleToEmotionObject, styleSet) { @@ -79,23 +82,10 @@ type ComposerCoreUIProps = { readonly storeDebugAPI?: StoreDebugAPI | undefined; }; -const ROOT_STYLE = { - '&.webchat__css-custom-properties': { - '& .webchat__live-region': { - color: 'transparent', - height: 1, - overflow: 'hidden', - position: 'absolute', - top: 0, - whiteSpace: 'nowrap', - width: 1 - } - } -}; - const ComposerCoreUI = memo(({ children, nonce, storeDebugAPI }: ComposerCoreUIProps) => { const [{ assistiveTechnologyAnnouncementTransport, internalLiveRegionFadeAfter }] = useStyleOptions(); - const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + ''; + // Keeps the "webchat__css-custom-properties" class name. + const composerClassNames = useStyles(styles, webChatPrefixedClassName); const rootRef = useRef(null); const trackException = useTrackException(); @@ -130,7 +120,7 @@ const ComposerCoreUI = memo(({ children, nonce, storeDebugAPI }: ComposerCoreUIP return ( diff --git a/packages/component/src/Styles/StyleSet/ModalDialog.ts b/packages/component/src/Styles/StyleSet/ModalDialog.ts index 592288e2ea..2ee26a8ce2 100644 --- a/packages/component/src/Styles/StyleSet/ModalDialog.ts +++ b/packages/component/src/Styles/StyleSet/ModalDialog.ts @@ -6,6 +6,11 @@ const LIGHT_THEME_SELECTOR = '@media (forced-colors: none)'; import CSSTokens from '../CSSTokens'; +/** + * @deprecated Not used for rendering since 4.19.3, customizing it has no effect. + * The live styles are in `packages/component/src/providers/ModalDialog/private/Popover.module.css`. + * This style set will be removed on or after 2028-10-16. + */ export default function createModalDialogStyleSet() { return { '&.webchat__modal-dialog': { diff --git a/packages/component/src/Styles/StyleSet/Tooltip.ts b/packages/component/src/Styles/StyleSet/Tooltip.ts index 66fd76d03c..a60965575a 100644 --- a/packages/component/src/Styles/StyleSet/Tooltip.ts +++ b/packages/component/src/Styles/StyleSet/Tooltip.ts @@ -1,3 +1,8 @@ +/** + * @deprecated Not used for rendering since 4.19.3, customizing it has no effect. + * The live styles are in `packages/component/src/Tooltip/private/Tooltip.module.css`. + * This style set will be removed on or after 2028-10-16. + */ export default () => ({ '*:has(> &.webchat__tooltip)': { position: 'relative', diff --git a/packages/component/src/Styles/createStyleSet.ts b/packages/component/src/Styles/createStyleSet.ts index 1ffd81555e..2d2508d368 100644 --- a/packages/component/src/Styles/createStyleSet.ts +++ b/packages/component/src/Styles/createStyleSet.ts @@ -153,6 +153,11 @@ export default function createStyleSet(styleOptions: StyleOptions) { * This style set will be removed on or after 2028-10-16. */ linkDefinitions: createLinkDefinitionsStyle(), + /** + * @deprecated Not used for rendering since 4.19.3, customizing it has no effect. + * The live styles are in `packages/component/src/providers/ModalDialog/private/Popover.module.css`. + * This style set will be removed on or after 2028-10-16. + */ modalDialog: createModalDialogStyle(), /** * @deprecated Not used for rendering since 4.19.0 (PR #5670), customizing it has no effect. @@ -178,6 +183,11 @@ export default function createStyleSet(styleOptions: StyleOptions) { * This style set will be removed on or after 2028-10-16. */ thumbButton: createThumbButtonStyle(), + /** + * @deprecated Not used for rendering since 4.19.3, customizing it has no effect. + * The live styles are in `packages/component/src/Tooltip/private/Tooltip.module.css`. + * This style set will be removed on or after 2028-10-16. + */ tooltip: createTooltipStyle() } as const); } diff --git a/packages/component/src/Tooltip/private/Tooltip.module.css b/packages/component/src/Tooltip/private/Tooltip.module.css new file mode 100644 index 0000000000..d17580161c --- /dev/null +++ b/packages/component/src/Tooltip/private/Tooltip.module.css @@ -0,0 +1,67 @@ +/* The tooltip positions itself against its parent, which can be any element. */ +:global(.webchat) :has(> .tooltip) { + position: relative; + + /* For -
-
{children}
+
+
- - ); - } -); +
{children}
+
+ + ); +} ModalDialog.displayName = 'ModalDialog'; -export default ModalDialog; +export default memo(ModalDialog); +export { modalDialogPropsSchema, type ModalDialogProps }; diff --git a/packages/fluent-theme/src/components/theme/Theme.module.css b/packages/fluent-theme/src/components/theme/Theme.module.css index ebb0305177..c0e9344c1a 100644 --- a/packages/fluent-theme/src/components/theme/Theme.module.css +++ b/packages/fluent-theme/src/components/theme/Theme.module.css @@ -187,129 +187,13 @@ --webchat-curveDecelerateMid: var(--curveDecelerateMid, cubic-bezier(0, 0, 0, 1)); } +.variant-copilot { + --webchat-preserve-reason: target-class; +} + @media (prefers-reduced-motion) { :global(.webchat-fluent).theme { --webchat-durationUltraFast: 0.01ms; --webchat-durationNormal: 0.01ms; } } - -/* Style Options override */ -:global(.webchat-fluent).theme :global(.webchat__css-custom-properties) { - --webchat__color--accent: var(--webchat-colorBrandForeground1); - --webchat__color--subtle: var(--webchat-colorNeutralForeground2); - --webchat__color--timestamp: var(--webchat-colorNeutralForeground3); - --webchat__font--primary: var(--webchat-fontFamilyBase); - --webchat__font-size--small: var(--webchat-fontSizeBase200); - --webchat__font-size--medium: var(--webchat-fontSizeBase300); - --webchat__line-height--small: var(--webchat-lineHeightBase200); - --webchat__line-height--medium: var(--webchat-lineHeightBase300); - --webchat__max-width--bubble: var(--webchat-bubble-maxWidth); - --webchat__max-width--attachment-bubble: var(--webchat-bubble-maxWidth); - --webchat__min-height--bubble: var(--webchat-bubble-minHeight); - --webchat__padding--regular: var(--webchat-spacingVerticalS); - --webchat__padding--sendbox: var(--webchat-spacingVerticalNone) var(--webchat-spacingHorizontalMNudge) - var(--webchat-spacingHorizontalMNudge); - --webchat__transition-duration: var(--webchat-durationNormal); - --webchat__transition-easing: var(--webchat-curveDecelerateMid); - --webchat__border-animation--color-1: #464feb; - --webchat__border-animation--color-2: #47cffa; - --webchat__border-animation--color-3: #b47cf8; -} - -:global(.webchat-fluent).theme.variant-copilot :global(.webchat__css-custom-properties) { - --webchat__padding--sendbox: var(--webchat-spacingVerticalNone) var(--webchat-spacingHorizontalL) - var(--webchat-spacingVerticalL) var(--webchat-spacingHorizontalSNudge); - --webchat__max-height--image-bubble: max(480px, 90cqb); -} - -/* Transcript surface */ -:global(.webchat-fluent).theme :global(.webchat__surface) { - background-color: var(--webchat__color--surface); -} - -:global(.webchat-fluent).theme.variant-copilot :global(.webchat__surface) { - box-sizing: border-box; - padding-inline-start: var(--webchat-spacingHorizontalMNudge); -} - -:global(.webchat-fluent).theme.variant-copilot - :global(.webchat__basic-transcript .webchat__basic-transcript__scrollable) { - scrollbar-gutter: stable; -} - -/* Transcript filer in copilot variant */ -:global(.webchat-fluent).theme.variant-copilot :global(.webchat__basic-transcript .webchat__basic-transcript__filler) { - /* No filler unless pre-chat activity, see next rule */ - flex-grow: 0; -} - -/* Transcript with a single pre-chat activity */ -:global(.webchat-fluent).theme - :global( - .webchat__basic-transcript:has(.part-grouping-decorator:only-child):has(.pre-chat-message-activity) - .webchat__basic-transcript__filler - ) { - /* limit grow to half of the size to center the activity */ - flex-grow: 0.5; -} - -/* Modal */ -:global(.webchat-fluent).theme :global(.webchat__modal-dialog) { - height: 100%; - display: grid; - place-items: center; - - &::backdrop { - background-color: var(--webchat-colorBackgroundOverlay); - } - - :global(.webchat__modal-dialog__box) { - background-color: var(--webchat-colorNeutralBackground1); - border-radius: var(--webchat-borderRadiusXLarge); - position: relative; - color: var(--webchat-colorNeutralForeground1); - box-shadow: var(--webchat-shadow64); - } - - :global(.webchat__modal-dialog__close-button) { - color: var(--webchat-colorNeutralForeground1); - transition: background-color var(--webchat-durationNormal) var(--webchat-curveDecelerateMid); - } - - /* TODO: align with original fluent modal styles in WebChat as they don't match v9 anymore */ - :global(.webchat__modal-dialog__close-button):not(:active):hover { - background-color: var(--webchat-colorNeutralBackground3); - } - - :global(.webchat__modal-dialog__close-button):focus:active { - background-color: var(--webchat-colorNeutralBackground4); - } - - :global(.webchat__modal-dialog__close-button):not(:focus-visible) { - border: none; - } - - :global(.webchat__modal-dialog__close-button):focus { - border-color: var(--webchat-colorStrokeFocus2); - } - - :global(.webchat__modal-dialog__close-button-icon) { - color: currentColor; - } -} - -/* Tooltip */ -:global(.webchat-fluent).theme :global(.webchat__tooltip) { - --webchat__tooltip-background: var(--tooltip-background, var(--webchat-colorNeutralBackground1)); - - color: var(--webchat-colorNeutralForeground1); - filter: drop-shadow(0 0 2px var(--webchat-colorNeutralShadowAmbient)) - drop-shadow(0 4px 8px var(--webchat-colorNeutralShadowKey)); - font-family: var(--webchat-fontFamilyBase); - font-size: var(--webchat-fontSizeBase200); - font-weight: var(--webchat-fontWeightRegular); - line-height: var(--webchat-lineHeightBase200); - padding: var(--webchat-spacingVerticalSNudge) var(--webchat-spacingHorizontalM); - transition: opacity var(--webchat-durationNormal) var(--webchat-curveDecelerateMid); -} diff --git a/packages/fluent-theme/src/private/fluentCSSModuleTheme.module.css b/packages/fluent-theme/src/private/fluentCSSModuleTheme.module.css index f7b4e3775d..64202c66ce 100644 --- a/packages/fluent-theme/src/private/fluentCSSModuleTheme.module.css +++ b/packages/fluent-theme/src/private/fluentCSSModuleTheme.module.css @@ -29,23 +29,23 @@ } } - .stacked-layout .bubble :global(.webchat__fileContent__badge) { + .stacked-layout .bubble .fileContent__badge { cursor: default; font-size: var(--webchat-fontSizeBase300); line-height: var(--webchat-lineHeightBase300); - :global(.webchat__fileContent__fileName) { + .fileContent__fileName { color: var(--webchat-colorBrandForegroundLink); font-family: inherit; } - :global(.webchat__fileContent__size) { + .fileContent__size { color: var(--webchat-colorNeutralForeground2); font-family: inherit; } } - .stacked-layout .bubble :global(.webchat__fileContent__downloadIcon) { + .stacked-layout .bubble .fileContent__downloadIcon { color: var(--webchat-colorBrandForegroundLink); } /* #endregion */ @@ -973,11 +973,50 @@ /* #region Theme */ +/* Style Options override */ +:global(.webchat-fluent).theme .css-custom-properties { + --webchat__color--accent: var(--webchat-colorBrandForeground1); + --webchat__color--subtle: var(--webchat-colorNeutralForeground2); + --webchat__color--timestamp: var(--webchat-colorNeutralForeground3); + --webchat__font--primary: var(--webchat-fontFamilyBase); + --webchat__font-size--small: var(--webchat-fontSizeBase200); + --webchat__font-size--medium: var(--webchat-fontSizeBase300); + --webchat__line-height--small: var(--webchat-lineHeightBase200); + --webchat__line-height--medium: var(--webchat-lineHeightBase300); + --webchat__max-width--bubble: var(--webchat-bubble-maxWidth); + --webchat__max-width--attachment-bubble: var(--webchat-bubble-maxWidth); + --webchat__min-height--bubble: var(--webchat-bubble-minHeight); + --webchat__padding--regular: var(--webchat-spacingVerticalS); + --webchat__padding--sendbox: var(--webchat-spacingVerticalNone) var(--webchat-spacingHorizontalMNudge) + var(--webchat-spacingHorizontalMNudge); + --webchat__transition-duration: var(--webchat-durationNormal); + --webchat__transition-easing: var(--webchat-curveDecelerateMid); + --webchat__border-animation--color-1: #464feb; + --webchat__border-animation--color-2: #47cffa; + --webchat__border-animation--color-3: #b47cf8; +} + +:global(.webchat-fluent).theme.variant-copilot .css-custom-properties { + --webchat__padding--sendbox: var(--webchat-spacingVerticalNone) var(--webchat-spacingHorizontalL) + var(--webchat-spacingVerticalL) var(--webchat-spacingHorizontalSNudge); + --webchat__max-height--image-bubble: max(480px, 90cqb); +} + +/* Transcript surface */ +:global(.webchat-fluent).theme .surface { + background-color: var(--webchat__color--surface); +} + +:global(.webchat-fluent).theme.variant-copilot .surface { + box-sizing: border-box; + padding-inline-start: var(--webchat-spacingHorizontalMNudge); +} + /* Scrollbars */ :global(.webchat-fluent).theme .code-block-content .code-block-content__code-block, :global(.webchat-fluent).theme .send-box-attachment-bar.send-box-attachment-bar--as-list-item, :global(.webchat-fluent).theme .text-area.text-area--scroll, -:global(.webchat-fluent).theme :global(.webchat__basic-transcript .webchat__basic-transcript__scrollable), +:global(.webchat-fluent).theme .basic-transcript .basic-transcript__scrollable, :global(.webchat-fluent).theme .render-markdown [data-math-type='block'], :global(.webchat-fluent).theme .view-code-dialog__body, :global(.webchat-fluent).theme :global(.view-code-dialog__code-body) { @@ -1029,6 +1068,10 @@ --webchat__transcript--spacing: var(--webchat-spacingVerticalM); } +:global(.webchat-fluent).theme.variant-copilot .basic-transcript .basic-transcript__scrollable { + scrollbar-gutter: stable; +} + /* Transcript focus indicator color */ :global(.webchat-fluent).theme .transcript-focus-area__transcript-indicator { --webchat__color--transcript-visual-keyboard-indicator: var(--webchat-colorStrokeFocus2); @@ -1056,7 +1099,7 @@ } /* Transcript activity focused directly */ - > :global(.webchat__focus-trap) > :global(.webchat__basic-transcript__activity-body) .bubble { + > .focus-trap > .basic-transcript__activity-body .bubble { display: grid; grid-template-areas: 'focused-content'; position: static; @@ -1124,6 +1167,67 @@ } } +/* Transcript filer in copilot variant */ +:global(.webchat-fluent).theme.variant-copilot .basic-transcript .basic-transcript__filler { + /* No filler unless pre-chat activity, see next rule */ + flex-grow: 0; +} + +/* Transcript with a single pre-chat activity */ +:global(.webchat-fluent).theme + .basic-transcript:has(:global(.part-grouping-decorator):only-child):has(:global(.pre-chat-message-activity)) + .basic-transcript__filler { + /* limit grow to half of the size to center the activity */ + flex-grow: 0.5; +} + +/* Modal */ +:global(.webchat-fluent).theme .modal-dialog { + height: 100%; + display: grid; + place-items: center; + + &::backdrop { + background-color: var(--webchat-colorBackgroundOverlay); + } + + .modal-dialog__box { + background-color: var(--webchat-colorNeutralBackground1); + border-radius: var(--webchat-borderRadiusXLarge); + position: relative; + color: var(--webchat-colorNeutralForeground1); + box-shadow: var(--webchat-shadow64); + } + + .modal-dialog__close-button { + color: var(--webchat-colorNeutralForeground1); + transition: background-color var(--webchat-durationNormal) var(--webchat-curveDecelerateMid); + } + + /* TODO: align with original fluent modal styles in WebChat as they don't match v9 anymore */ + /* Popover.module.css can come later in the stylesheet: the ":focus" selector out-specifies its focused hover rule. */ + .modal-dialog__close-button:not(:active):hover, + .modal-dialog__close-button:focus:not(:active):hover { + background-color: var(--webchat-colorNeutralBackground3); + } + + .modal-dialog__close-button:focus:active { + background-color: var(--webchat-colorNeutralBackground4); + } + + .modal-dialog__close-button:not(:focus-visible) { + border: none; + } + + .modal-dialog__close-button:focus { + border-color: var(--webchat-colorStrokeFocus2); + } + + .modal-dialog__close-button-icon { + color: currentColor; + } +} + /* Code block */ :global(.webchat-fluent).theme .code-block { border: none; @@ -1205,13 +1309,13 @@ } /* View Code dialog */ -:global(.webchat-fluent).theme :global(.webchat__modal-dialog).view-code-dialog { - :global(.webchat__modal-dialog__close-button-layout) { +:global(.webchat-fluent).theme .modal-dialog.view-code-dialog { + .modal-dialog__close-button-layout { display: flex; padding: var(--webchat-spacingVerticalS) var(--webchat-spacingHorizontalS); } - :global(.webchat__modal-dialog__close-button) { + .modal-dialog__close-button { align-items: center; aspect-ratio: 1; display: flex; @@ -1231,11 +1335,11 @@ top: 0; } - :global(.webchat__modal-dialog__box) { + .modal-dialog__box { width: fit-content; } - :global(.webchat__modal-dialog__body) { + .modal-dialog__body { margin: 0; padding: var(--webchat-spacingVerticalS) var(--webchat-spacingHorizontalM); } @@ -1355,6 +1459,21 @@ } } +/* Tooltip */ +:global(.webchat-fluent).theme .tooltip { + --webchat__tooltip-background: var(--tooltip-background, var(--webchat-colorNeutralBackground1)); + + color: var(--webchat-colorNeutralForeground1); + filter: drop-shadow(0 0 2px var(--webchat-colorNeutralShadowAmbient)) + drop-shadow(0 4px 8px var(--webchat-colorNeutralShadowKey)); + font-family: var(--webchat-fontFamilyBase); + font-size: var(--webchat-fontSizeBase200); + font-weight: var(--webchat-fontWeightRegular); + line-height: var(--webchat-lineHeightBase200); + padding: var(--webchat-spacingVerticalSNudge) var(--webchat-spacingHorizontalM); + transition: opacity var(--webchat-durationNormal) var(--webchat-curveDecelerateMid); +} + /* Feedback Form */ :global(.webchat-fluent).theme .feedback-form { .feedback-form__form-header { diff --git a/packages/vibe-grep/src/rules/lightning-css.yaml b/packages/vibe-grep/src/rules/lightning-css.yaml index a163f6b026..c1e5e2ea0b 100644 --- a/packages/vibe-grep/src/rules/lightning-css.yaml +++ b/packages/vibe-grep/src/rules/lightning-css.yaml @@ -3,6 +3,7 @@ # - Starting from .webchat or .webchat- # - Followed by optional modifier class # - Followed by zero or more spaces +# - Followed by an optional ":has(> ", for a rule on the parent of a class (e.g. the tooltip) # - Followed by a class selector with a 6-character file hash # - Followed by an underscore @@ -11,7 +12,7 @@ language: Css rule: kind: selectors > descendant_selector not: - regex: "\\.(webchat|webchat-\\w*)(\\.\\w+--[\\w-]+)?\\s*\\.w[a-zA-Z0-9\\-_]{6}_" + regex: "\\.(webchat|webchat-\\w*)(\\.\\w+--[\\w-]+)?\\s*(:has\\(>\\s*)?\\.w[a-zA-Z0-9\\-_]{6}_" description: "Check if CSS is compiled by Lightning CSS plugin conforms to expected selector pattern" message: "Output CSS does not conform to Lightning CSS selector pattern. Check that the CSS file is compiled correctly."