From 13a069c431e5b06950cf974bb99bb6ee6c60179f Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Thu, 14 Aug 2025 10:40:37 +0200 Subject: [PATCH 1/3] version 1 --- pages/app/templates.tsx | 2 - pages/chat-bubble/selectable.page.tsx | 82 +++++++++++++++++++ .../__snapshots__/documenter.test.ts.snap | 28 ++++++- src/chat-bubble/index.tsx | 1 + src/chat-bubble/interfaces.ts | 17 ++++ src/chat-bubble/internal.tsx | 16 +++- src/chat-bubble/styles.scss | 32 +++++++- tsconfig.json | 2 +- 8 files changed, 173 insertions(+), 7 deletions(-) create mode 100644 pages/chat-bubble/selectable.page.tsx diff --git a/pages/app/templates.tsx b/pages/app/templates.tsx index 41a653b..52c6800 100644 --- a/pages/app/templates.tsx +++ b/pages/app/templates.tsx @@ -39,8 +39,6 @@ export function Page({ navigationHide={true} activeDrawerId={toolsOpen ? "settings" : null} onDrawerChange={({ detail }) => setToolsOpen(!!detail.activeDrawerId)} - tools={settings && Page settings}>{settings}} - toolsHide={!settings} drawers={drawers} content={ diff --git a/pages/chat-bubble/selectable.page.tsx b/pages/chat-bubble/selectable.page.tsx new file mode 100644 index 0000000..df8a33b --- /dev/null +++ b/pages/chat-bubble/selectable.page.tsx @@ -0,0 +1,82 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { useState } from "react"; + +import Box from "@cloudscape-design/components/box"; +import Button from "@cloudscape-design/components/button"; +import Container from "@cloudscape-design/components/container"; +import Header from "@cloudscape-design/components/header"; +import SpaceBetween from "@cloudscape-design/components/space-between"; + +import { ChatBubble } from "../../lib/components"; +import { Page } from "../app/templates"; +import { ChatBubbleAvatarGenAI, ChatBubbleAvatarUser } from "./util-components"; + +export default function ChatBubbleSelectablePage() { + const [selectedCardId, setSelectedCardId] = useState(null); + return ( + + + Chat container}> + + } type="outgoing" ariaLabel="User at 4:24:12pm"> + User request example + + + } type="incoming" ariaLabel="Gen AI at 4:24:24pm"> + Normal response example + + + } + hideAvatar={true} + type="incoming" + ariaLabel="Gen AI at 4:24:25pm" + selectionType="click" + onSelect={() => setSelectedCardId((prev) => (prev !== "1" ? "1" : null))} + selected={selectedCardId === "1"} + > + Selectable response example + This entire card is clickable + + + } + hideAvatar={true} + type="incoming" + ariaLabel="Gen AI at 4:24:25pm" + selectionType="custom" + selected={selectedCardId === "2"} + > +
+ Selectable response example + {selectedCardId !== "2" ? ( + + ) : ( + + )} +
+ + This entire card is not clickable. Instead, there is a custom action in the top-right. + +
+
+
+ + Presentation container}> + {selectedCardId ? ( + + {selectedCardId === "1" ? "First" : "Second"} card is selected + + + ) : null} + +
+
+ ); +} diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index c3276f4..44e4128 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -201,7 +201,13 @@ Use this property if the icon you want isn't available.", exports[`definition for chat-bubble matches the snapshot > chat-bubble 1`] = ` { "dashCaseName": "chat-bubble", - "events": [], + "events": [ + { + "cancelable": false, + "description": "POC", + "name": "onSelect", + }, + ], "functions": [], "name": "ChatBubble", "properties": [ @@ -219,6 +225,26 @@ Useful for when there are multiple consecutive messages coming from the same aut "optional": true, "type": "boolean", }, + { + "description": "POC", + "name": "selected", + "optional": true, + "type": "boolean", + }, + { + "description": "POC", + "inlineType": { + "name": ""click" | "custom"", + "type": "union", + "values": [ + "click", + "custom", + ], + }, + "name": "selectionType", + "optional": true, + "type": "string", + }, { "description": "Adds a loading bar to the bottom of the chat bubble. This property should only be used for Generative AI loading state. If avatar is being used, set its \`loading\` state to true.", diff --git a/src/chat-bubble/index.tsx b/src/chat-bubble/index.tsx index 9067b2b..45a2386 100644 --- a/src/chat-bubble/index.tsx +++ b/src/chat-bubble/index.tsx @@ -1,5 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 + import useBaseComponent from "../internal/base-component/use-base-component"; import { applyDisplayName } from "../internal/utils/apply-display-name"; import { ChatBubbleProps } from "./interfaces"; diff --git a/src/chat-bubble/interfaces.ts b/src/chat-bubble/interfaces.ts index 068f48c..71551e0 100644 --- a/src/chat-bubble/interfaces.ts +++ b/src/chat-bubble/interfaces.ts @@ -1,6 +1,8 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +import { NonCancelableEventHandler } from "../internal/events"; + export interface ChatBubbleProps { /** Avatar slot paired with the chat bubble content. Use [avatar](/components/avatar/). */ avatar: React.ReactNode; @@ -31,6 +33,21 @@ export interface ChatBubbleProps { * Useful for when there are multiple consecutive messages coming from the same author. */ hideAvatar?: boolean; + + /** + * POC + */ + selectionType?: "click" | "custom"; + + /** + * POC + */ + selected?: boolean; + + /** + * POC + */ + onSelect?: NonCancelableEventHandler; } export namespace ChatBubbleProps { diff --git a/src/chat-bubble/internal.tsx b/src/chat-bubble/internal.tsx index bd3e798..87dea00 100644 --- a/src/chat-bubble/internal.tsx +++ b/src/chat-bubble/internal.tsx @@ -1,10 +1,12 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 + import { useEffect, useRef } from "react"; import clsx from "clsx"; import { getDataAttributes } from "../internal/base-component/get-data-attributes"; import { InternalBaseComponentProps } from "../internal/base-component/use-base-component"; +import { fireNonCancelableEvent } from "../internal/events"; import { InternalLoadingBar } from "../loading-bar/internal"; import { ChatBubbleProps } from "./interfaces.js"; @@ -20,6 +22,9 @@ export default function InternalChatBubble({ showLoadingBar, hideAvatar = false, ariaLabel, + selectionType, + selected, + onSelect, __internalRootRef = null, ...rest }: InternalChatBubbleProps) { @@ -36,6 +41,8 @@ export default function InternalChatBubble({ } }, [hideAvatar]); + const Tag = selectionType === "click" && onSelect ? "button" : "div"; + return (
)} -
fireNonCancelableEvent(onSelect)} className={clsx(styles["message-area"], styles[`chat-bubble-type-${type}`], { [styles["with-loading-bar"]]: showLoadingBar, + [styles["message-area-selectable"]]: !!selectionType, + [styles["message-area-clickable"]]: selectionType === "click", + [styles["message-area-selected"]]: selected, })} >
{children}
@@ -60,7 +72,7 @@ export default function InternalChatBubble({ {actions &&
{actions}
} {showLoadingBar && } -
+
); } diff --git a/src/chat-bubble/styles.scss b/src/chat-bubble/styles.scss index cf79209..1b522ea 100644 --- a/src/chat-bubble/styles.scss +++ b/src/chat-bubble/styles.scss @@ -18,12 +18,14 @@ } .message-area { + @include shared.styles-reset; + display: flex; flex-direction: column; gap: cs.$space-scaled-s; padding: cs.$space-scaled-s; min-width: 0; - overflow-x: auto; + // overflow-x: auto; border-start-start-radius: cs.$border-radius-chat-bubble; border-start-end-radius: cs.$border-radius-chat-bubble; @@ -39,6 +41,34 @@ background-color: cs.$color-background-chat-bubble-outgoing; } + // TODO: create new tokens for outgoing-selectable (background, border, selected background, selected border) + // Potentially, need some for border width, too. + &.message-area-selectable.message-area-selectable { + border-color: cs.$color-border-divider-default; + border-width: 2px; + border-style: solid; + background-color: cs.$color-background-layout-main; + } + + &.message-area-clickable.message-area-clickable { + cursor: pointer; + + &:focus-visible { + @include shared.focus-highlight(6px, cs.$border-radius-chat-bubble); + } + + &:hover { + background-color: cs.$color-background-dropdown-item-hover; + } + } + + &.message-area-selected.message-area-selected.message-area-selected { + border-color: cs.$color-border-item-selected; + border-width: 2px; + border-style: solid; + background-color: cs.$color-background-item-selected; + } + &.chat-bubble-type-incoming { color: cs.$color-text-chat-bubble-incoming; background-color: cs.$color-background-chat-bubble-incoming; diff --git a/tsconfig.json b/tsconfig.json index e7229a3..ca91991 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "types": [], "lib": ["es2019", "dom", "dom.iterable"], "module": "ESNext", - "moduleResolution": "nodenext", + "moduleResolution": "Node", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true, From dda9a97339daecbd69b3ada0b059c8ea8aad28d0 Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Fri, 15 Aug 2025 08:12:30 +0200 Subject: [PATCH 2/3] version 2 --- .../support-prompt-group/selectable.page.tsx | 108 ++++++++++++++++++ .../__snapshots__/documenter.test.ts.snap | 11 ++ src/chat-bubble/interfaces.ts | 5 + src/chat-bubble/internal.tsx | 33 +++--- src/chat-bubble/styles.scss | 1 + src/internal/shared.scss | 19 +++ src/support-prompt-group/interfaces.ts | 8 +- src/support-prompt-group/internal.tsx | 10 +- src/support-prompt-group/prompt.tsx | 47 ++++---- src/support-prompt-group/styles.scss | 14 +++ 10 files changed, 218 insertions(+), 38 deletions(-) create mode 100644 pages/support-prompt-group/selectable.page.tsx diff --git a/pages/support-prompt-group/selectable.page.tsx b/pages/support-prompt-group/selectable.page.tsx new file mode 100644 index 0000000..204aac4 --- /dev/null +++ b/pages/support-prompt-group/selectable.page.tsx @@ -0,0 +1,108 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { useContext, useState } from "react"; + +import Box from "@cloudscape-design/components/box"; +import Button from "@cloudscape-design/components/button"; +import Checkbox from "@cloudscape-design/components/checkbox"; +import Container from "@cloudscape-design/components/container"; +import Header from "@cloudscape-design/components/header"; +import Icon from "@cloudscape-design/components/icon"; +import SpaceBetween from "@cloudscape-design/components/space-between"; + +import { ChatBubble, SupportPromptGroup } from "../../lib/components"; +import AppContext, { AppContextType } from "../app/app-context"; +import { Page } from "../app/templates"; +import { ChatBubbleAvatarGenAI, ChatBubbleAvatarUser } from "../chat-bubble/util-components"; + +interface PageSettings { + alignment: "horizontal" | "vertical"; +} + +type PageContext = React.Context>>; + +export default function ChatBubbleSelectablePage() { + const { + urlParams: { alignment = "horizontal" }, + setUrlParams, + } = useContext(AppContext as PageContext); + const [selectedCardId, setSelectedCardId] = useState(null); + return ( + + setUrlParams({ alignment: detail.checked ? "vertical" : "horizontal" })} + > + Use vertical prompts + + + } + > + + Chat container}> + + } type="outgoing" ariaLabel="User at 4:24:12pm"> + User request example + + + } type="incoming" ariaLabel="Gen AI at 4:24:24pm"> + Normal response example + + + } + hideAvatar={true} + type="incoming" + ariaLabel="Gen AI at 4:24:25pm" + additionalContent={ + + detail.id === selectedCardId ? setSelectedCardId(null) : setSelectedCardId(detail.id) + } + toggledItems={selectedCardId ? [selectedCardId] : []} + items={[ + { + id: "1", + text: "Expandable option 1 content", + header: { + text: "Expandable option 1", + icon: , + pressedIcon: , + }, + }, + { + id: "2", + text: "Expandable option 2 content", + header: { + text: "Expandable option 2", + icon: , + pressedIcon: , + }, + }, + ]} + /> + } + > + Selectable response example + + + + + Presentation container}> + {selectedCardId ? ( + + Option {selectedCardId} is selected + + + ) : null} + + + + ); +} diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap index 44e4128..2979c9b 100644 --- a/src/__tests__/__snapshots__/documenter.test.ts.snap +++ b/src/__tests__/__snapshots__/documenter.test.ts.snap @@ -273,6 +273,11 @@ If avatar is being used, set its \`loading\` state to true.", "isDefault": false, "name": "actions", }, + { + "description": "POC", + "isDefault": false, + "name": "additionalContent", + }, { "description": "Avatar slot paired with the chat bubble content. Use [avatar](/components/avatar/).", "isDefault": false, @@ -409,6 +414,12 @@ Each item has the following properties: "optional": false, "type": "ReadonlyArray", }, + { + "description": "POC", + "name": "toggledItems", + "optional": true, + "type": "Array", + }, ], "regions": [], "releaseStatus": "stable", diff --git a/src/chat-bubble/interfaces.ts b/src/chat-bubble/interfaces.ts index 71551e0..425035e 100644 --- a/src/chat-bubble/interfaces.ts +++ b/src/chat-bubble/interfaces.ts @@ -13,6 +13,11 @@ export interface ChatBubbleProps { /** Content of the chat bubble */ children: React.ReactNode; + /** + * POC + */ + additionalContent?: React.ReactNode; + /** Actions slot of the chat bubble, placed at the footer. Use [button group](/components/button-group/). */ actions?: React.ReactNode; diff --git a/src/chat-bubble/internal.tsx b/src/chat-bubble/internal.tsx index 87dea00..1d26fcb 100644 --- a/src/chat-bubble/internal.tsx +++ b/src/chat-bubble/internal.tsx @@ -17,6 +17,7 @@ export interface InternalChatBubbleProps extends ChatBubbleProps, InternalBaseCo export default function InternalChatBubble({ type, children, + additionalContent, avatar, actions, showLoadingBar, @@ -57,22 +58,26 @@ export default function InternalChatBubble({ )} - fireNonCancelableEvent(onSelect)} - className={clsx(styles["message-area"], styles[`chat-bubble-type-${type}`], { - [styles["with-loading-bar"]]: showLoadingBar, - [styles["message-area-selectable"]]: !!selectionType, - [styles["message-area-clickable"]]: selectionType === "click", - [styles["message-area-selected"]]: selected, - })} - > -
{children}
+
+ fireNonCancelableEvent(onSelect)} + className={clsx(styles["message-area"], styles[`chat-bubble-type-${type}`], { + [styles["with-loading-bar"]]: showLoadingBar, + [styles["message-area-selectable"]]: !!selectionType, + [styles["message-area-clickable"]]: selectionType === "click", + [styles["message-area-selected"]]: selected, + })} + > +
{children}
- {actions &&
{actions}
} + {actions &&
{actions}
} - {showLoadingBar && } -
+ {showLoadingBar && } + + + {additionalContent &&
{additionalContent}
} +
); } diff --git a/src/chat-bubble/styles.scss b/src/chat-bubble/styles.scss index 1b522ea..482f8e7 100644 --- a/src/chat-bubble/styles.scss +++ b/src/chat-bubble/styles.scss @@ -19,6 +19,7 @@ .message-area { @include shared.styles-reset; + @include shared.default-text-style; display: flex; flex-direction: column; diff --git a/src/internal/shared.scss b/src/internal/shared.scss index 4d762dc..b959c9c 100644 --- a/src/internal/shared.scss +++ b/src/internal/shared.scss @@ -4,6 +4,23 @@ */ @use "../../node_modules/@cloudscape-design/design-tokens/index.scss" as cs; +$font-family-base: cs.$font-family-base; +$font-weight-normal: 400; + +@mixin default-text-style { + @include font-body-m; + color: cs.$color-text-body-default; + font-weight: $font-weight-normal; + font-family: $font-family-base; + -webkit-font-smoothing: auto; + -moz-osx-font-smoothing: auto; +} + +@mixin font-body-m { + font-size: cs.$font-size-body-m; + line-height: cs.$line-height-body-m; +} + @mixin focus-highlight( $gutter: 4px, $border-radius: cs.$border-radius-control-default-focus-ring, @@ -44,12 +61,14 @@ animation: none; transition: none; } + :global(.awsui-motion-disabled) &, :global(.awsui-mode-entering) & { animation: none; transition: none; } } + /* stylelint-enable @cloudscape-design/no-motion-outside-of-mixin, selector-combinator-disallowed-list, selector-pseudo-class-no-unknown, selector-class-pattern */ @mixin styles-reset { diff --git a/src/support-prompt-group/interfaces.ts b/src/support-prompt-group/interfaces.ts index 61e437a..34edb1c 100644 --- a/src/support-prompt-group/interfaces.ts +++ b/src/support-prompt-group/interfaces.ts @@ -27,14 +27,20 @@ export interface SupportPromptGroupProps { * Use this to provide a unique accessible name for each support prompt group on the page. */ ariaLabel: string; + + /** + * POC + */ + toggledItems?: string[]; } export namespace SupportPromptGroupProps { export type Alignment = "vertical" | "horizontal"; export interface Item { - text: string; id: string; + text: string; + header?: { text: string; icon?: React.ReactNode; pressedIcon?: React.ReactNode }; } export interface ItemClickDetail extends _ClickDetail { diff --git a/src/support-prompt-group/internal.tsx b/src/support-prompt-group/internal.tsx index 4e113da..388f904 100644 --- a/src/support-prompt-group/internal.tsx +++ b/src/support-prompt-group/internal.tsx @@ -31,6 +31,7 @@ export const InternalSupportPromptGroup = forwardRef( items, __internalRootRef, ariaLabel, + toggledItems, ...rest }: SupportPromptGroupProps & InternalBaseComponentProps, ref: Ref, @@ -154,9 +155,16 @@ export const InternalSupportPromptGroup = forwardRef( key={index} onClick={(event) => handleClick(event, item.id)} id={item.id} + pressed={toggledItems?.includes(item.id)} ref={(element) => (itemsRef.current[item.id] = element)} > - {item.text} + {item.header && ( +
+
{item.header.text}
+
{toggledItems?.includes(item.id) ? item.header.pressedIcon : item.header.icon}
+
+ )} +
{item.text}
); })} diff --git a/src/support-prompt-group/prompt.tsx b/src/support-prompt-group/prompt.tsx index 693ef78..ac3e4b4 100644 --- a/src/support-prompt-group/prompt.tsx +++ b/src/support-prompt-group/prompt.tsx @@ -10,31 +10,34 @@ import useForwardFocus from "../internal/utils/use-forward-focus"; import styles from "./styles.css.js"; export interface PromptProps { - children: string; + children: React.ReactNode; id: string; + pressed?: boolean; onClick: (event: React.MouseEvent, id: string) => void; } -export const Prompt = forwardRef(({ children, id, onClick }: PromptProps, ref: Ref) => { - const buttonRef = useRef(null); - useForwardFocus(ref, buttonRef); +export const Prompt = forwardRef( + ({ children, id, pressed, onClick }: PromptProps, ref: Ref) => { + const buttonRef = useRef(null); + useForwardFocus(ref, buttonRef); - const { tabIndex } = useSingleTabStopNavigation(buttonRef); + const { tabIndex } = useSingleTabStopNavigation(buttonRef); - return ( - - ); -}); + return ( + + ); + }, +); diff --git a/src/support-prompt-group/styles.scss b/src/support-prompt-group/styles.scss index 6b780e1..63e314c 100644 --- a/src/support-prompt-group/styles.scss +++ b/src/support-prompt-group/styles.scss @@ -51,6 +51,12 @@ border-color: awsui.$color-border-button-normal-active; } + &-pressed { + color: awsui.$color-text-button-normal-active; + background: awsui.$color-background-button-normal-active; + border-color: awsui.$color-border-button-normal-active; + } + &:focus { outline: none; } @@ -59,3 +65,11 @@ @include shared.focus-highlight(6px, 6px); } } + +.item-header { + display: flex; + gap: awsui.$space-static-xs; + justify-content: space-between; + + font-weight: bold; +} From f2b14829a55c91cfd442cef8a4aceb58aeab09ff Mon Sep 17 00:00:00 2001 From: Andrei Zhaleznichenka Date: Fri, 15 Aug 2025 09:10:10 +0200 Subject: [PATCH 3/3] version 3 --- .../selectable-custom-card.page.tsx | 111 ++++++++++++++++++ pages/chat-bubble/styles.module.scss | 91 ++++++++++++++ 2 files changed, 202 insertions(+) create mode 100644 pages/chat-bubble/selectable-custom-card.page.tsx create mode 100644 pages/chat-bubble/styles.module.scss diff --git a/pages/chat-bubble/selectable-custom-card.page.tsx b/pages/chat-bubble/selectable-custom-card.page.tsx new file mode 100644 index 0000000..5e5ba06 --- /dev/null +++ b/pages/chat-bubble/selectable-custom-card.page.tsx @@ -0,0 +1,111 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { useState } from "react"; +import clsx from "clsx"; + +import Box from "@cloudscape-design/components/box"; +import Button from "@cloudscape-design/components/button"; +import Container from "@cloudscape-design/components/container"; +import Header from "@cloudscape-design/components/header"; +import Icon, { IconProps } from "@cloudscape-design/components/icon"; +import SpaceBetween from "@cloudscape-design/components/space-between"; + +import { ChatBubble } from "../../lib/components"; +import { Page } from "../app/templates"; +import { ChatBubbleAvatarGenAI, ChatBubbleAvatarUser } from "./util-components"; + +import styles from "./styles.module.scss"; + +export default function ChatBubbleSelectableCustomPage() { + const [selectedCardId, setSelectedCardId] = useState(null); + return ( + + + Chat container}> + + } type="outgoing" ariaLabel="User at 4:24:12pm"> + User request example + + + } type="incoming" ariaLabel="Gen AI at 4:24:24pm"> + Normal response example + + + } + hideAvatar={true} + type="incoming" + ariaLabel="Gen AI at 4:24:25pm" + additionalContent={ + + setSelectedCardId(selectedCardId === "1" ? null : "1")} + > + Selectable card 1 content + + setSelectedCardId(selectedCardId === "2" ? null : "2")} + > + Selectable card 2 content + + + } + > + Selectable response example + + + + + Presentation container}> + {selectedCardId ? ( + + {selectedCardId === "1" ? "First" : "Second"} card is selected + + + ) : null} + + + + ); +} + +function CustomSelectableCard({ + header, + children, + iconName, + iconNamePressed, + pressed, + onClick, +}: { + header: React.ReactNode; + children: React.ReactNode; + iconName: IconProps.Name; + iconNamePressed: IconProps.Name; + pressed: boolean; + onClick: () => void; +}) { + return ( + + ); +} diff --git a/pages/chat-bubble/styles.module.scss b/pages/chat-bubble/styles.module.scss new file mode 100644 index 0000000..e558a11 --- /dev/null +++ b/pages/chat-bubble/styles.module.scss @@ -0,0 +1,91 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 +*/ + +@use "../../node_modules/@cloudscape-design/design-tokens/index.scss" as cs; + +@mixin styles-reset { + border-collapse: separate; + border-spacing: 0; + box-sizing: border-box; + caption-side: top; + cursor: auto; + direction: inherit; + empty-cells: show; + font-family: serif; + font-size: medium; + font-style: normal; + font-variant: normal; + font-weight: 400; + font-stretch: normal; + line-height: normal; + hyphens: none; + letter-spacing: normal; + list-style: disc outside none; + tab-size: 8; + text-align: start; + text-indent: 0; + text-shadow: none; + text-transform: none; + visibility: visible; + white-space: normal; + word-spacing: normal; +} + +@mixin focus-highlight( + $gutter: 4px, + $border-radius: cs.$border-radius-control-default-focus-ring, + $border-color: cs.$color-border-item-focused, + $border-width: 2px +) { + position: relative; + box-sizing: border-box; + outline: 2px dotted transparent; + outline-offset: calc($gutter - 1px); + + &::before { + content: " "; + display: block; + position: absolute; + box-sizing: border-box; + inset-inline-start: calc(-1 * #{$gutter}); + inset-block-start: calc(-1 * #{$gutter}); + inline-size: calc(100% + 2 * #{$gutter}); + block-size: calc(100% + 2 * #{$gutter}); + border-radius: $border-radius; + border: $border-width solid $border-color; + } +} + +.selectable-card { + @include styles-reset; + background: cs.$color-background-layout-main; + border-radius: 8px; + border: 2px solid cs.$color-border-divider-default; + padding: cs.$space-scaled-s cs.$space-scaled-s; + cursor: pointer; + + &:hover { + background: cs.$color-background-dropdown-item-hover; + } + + &-pressed { + border-color: cs.$color-border-item-selected; + background: cs.$color-background-item-selected; + + &:hover { + background: cs.$color-background-item-selected; + } + } + + &:focus-visible { + @include focus-highlight(8px, 8px); + } +} + +.selectable-card-header { + display: flex; + justify-content: space-between; + gap: cs.$space-static-s; +}