diff --git a/apps/pie-storybook/stories/pie-avatar.stories.ts b/apps/pie-storybook/stories/pie-avatar.stories.ts index 619f6a20e4..a8a5065659 100644 --- a/apps/pie-storybook/stories/pie-avatar.stories.ts +++ b/apps/pie-storybook/stories/pie-avatar.stories.ts @@ -6,6 +6,7 @@ import { type AvatarProps, defaultProps, tags } from '@justeattakeaway/pie-webc/ import { ifDefined } from 'lit/directives/if-defined.js'; import { createStory } from '../utilities'; +import '@justeattakeaway/pie-thumbnail'; type AvatarStoryMeta = Meta; @@ -15,20 +16,7 @@ const avatarStoryMeta: AvatarStoryMeta = { title: 'Components/Avatar', component: 'pie-avatar', argTypes: { - label: { - description: 'The name to display in the Avatar as initials. Should be a username, first and last name or company name.', - control: 'text', - }, - tag: { - description: 'Set the element tag of the avatar.', - control: 'select', - options: tags, - defaultValue: { - summary: defaultProps.tag, - }, - }, - src: { - description: 'Used to load an image to display inside the Avatar', + type: { control: 'text', }, }, @@ -45,9 +33,77 @@ const avatarStoryMeta: AvatarStoryMeta = { export default avatarStoryMeta; const Template = ({ label, tag, src }: AvatarProps) => html` - +
+ + + + + + +
+
+ + + + 3rd thing + 4rd thing + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +

carrot

+ + + +
+
`; +// +// +// +// + const createAvatarStory = createStory(Template, defaultArgs); export const Default = createAvatarStory(); diff --git a/apps/pie-storybook/stories/pie-list.stories.ts b/apps/pie-storybook/stories/pie-list.stories.ts index a06a9a00eb..bd0b2635e2 100644 --- a/apps/pie-storybook/stories/pie-list.stories.ts +++ b/apps/pie-storybook/stories/pie-list.stories.ts @@ -2,6 +2,12 @@ import { html } from 'lit'; import { type Meta } from '@storybook/web-components'; import '@justeattakeaway/pie-webc/components/list'; +import '@justeattakeaway/pie-webc/components/list-item'; +import '@justeattakeaway/pie-webc/components/thumbnail'; +import '@justeattakeaway/pie-webc/components/tag'; +import '@justeattakeaway/pie-webc/components/switch'; +import '@justeattakeaway/pie-icons-webc/dist/IconPlaceholder.js'; + import { type ListProps } from '@justeattakeaway/pie-webc/components/list'; import { createStory } from '../utilities'; @@ -28,7 +34,176 @@ export default listStoryMeta; // TODO: remove the eslint-disable rule when props are added // eslint-disable-next-line no-empty-pattern const Template = ({}: ListProps) => html` - +
+

Single item list

+ + + + Primary text + Secondary text + Meta text + + + +

Multi-item lists

+ + + + Primary text + Secondary text + Meta text + + + + + Primary text + Secondary text + Meta text + + + + + Primary text + Secondary text + Meta text + + + + + Primary text + Secondary text + Meta text + + + +

Radio group Trailing

+ + + + + Details for option one + + + + + + + Details for option two + + + + + + + Details for option three + + + + + + + Details for option four + + + + +

Radio group Leading

+ + + + + Visa, Mastercard, Amex + Card + + + + + + Pay with your PayPal account balance + Online + + + + + + Fast checkout using your Apple Wallet + Apple + + + + + + Checkout securely with saved cards + Google + + + + + + Direct wire from your checking account + Bank + + + +

Switch group Trailing

+ + + + Details for option one + + + + + + Details for option two + + + + + + Details for option three + + + + + + Details for option four + + + + +

Checkbox group

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
`; export const Default = createStory(Template, defaultArgs)(); diff --git a/packages/components/pie-avatar/src/avatar.scss b/packages/components/pie-avatar/src/avatar.scss index 8e11dd0203..5a7ce2652f 100644 --- a/packages/components/pie-avatar/src/avatar.scss +++ b/packages/components/pie-avatar/src/avatar.scss @@ -3,36 +3,67 @@ :host { // Note: For consistency sake, the recommended display should be either // "block" or "inline-block", otherwise it can be ommited for floating elements - display: block; - + display: flex; --avatar-size: 32px; + border: 1px solid purple; + min-height: 48px; } -.c-avatar-visuallyHidden { - @include p.visually-hidden; -} -.c-avatar { - display: flex; - justify-content: center; - align-items: center; - font-family: var(--dt-font-body-s-family); - @include p.font-size(--dt-font-body-s-size); - background-color: var(--dt-color-container-inverse); - color: var(--dt-color-content-interactive-primary); - width: var(--avatar-size); - height: var(--avatar-size); - border-radius:var(--dt-radius-rounded-e); - overflow: hidden; -} +:host([type="checkbox"]) { + ::slotted(label) { + flex: 1; + display: flex; + align-items: center; + justify-content: flex-start; + padding: 5px 10px; + background: lightblue; + gap: 1em; + } -.c-avatar--image { - width: 100%; - height: 100%; - object-fit: cover; + ::slotted(*:nth-child(n+3):last-child) { + margin-inline-start: auto; + } } -.c-avatar--button { - border: none; - font-family: inherit; -} \ No newline at end of file +// :host([type="checkbox"]) { +// ::slotted(label)::after { +// content: ''; +// position: absolute; +// inset: 0; +// background-color: lightgreen; +// } +// } + +@include p.radio-slotted-input-base('input[type="radio"]'); +@include p.radio-slotted-interactive-state('input[type="radio"]', 'dt-color-interactive-brand'); +@include p.radio-slotted-animations('input[type="radio"]'); + +// .c-avatar-visuallyHidden { +// @include p.visually-hidden; +// } + +// .c-avatar { +// display: flex; +// justify-content: center; +// align-items: center; +// font-family: var(--dt-font-body-s-family); +// @include p.font-size(--dt-font-body-s-size); +// background-color: var(--dt-color-container-inverse); +// color: var(--dt-color-content-interactive-primary); +// width: var(--avatar-size); +// height: var(--avatar-size); +// border-radius:var(--dt-radius-rounded-e); +// overflow: hidden; +// } + +// .c-avatar--image { +// width: 100%; +// height: 100%; +// object-fit: cover; +// } + +// .c-avatar--button { +// border: none; +// font-family: inherit; +// } diff --git a/packages/components/pie-avatar/src/defs.ts b/packages/components/pie-avatar/src/defs.ts index 5ae9fe93ff..6b84e31d8e 100644 --- a/packages/components/pie-avatar/src/defs.ts +++ b/packages/components/pie-avatar/src/defs.ts @@ -8,6 +8,8 @@ export interface AvatarProps { */ label?: string; + type?: string; + /** * What HTML element the avatar should be such as button, a or div. */ @@ -23,6 +25,7 @@ export interface AvatarProps { export type DefaultProps = ComponentDefaultProps>; export const defaultProps: DefaultProps = { tag: 'div', + type: 'ordered-list', }; export type Initials = { diff --git a/packages/components/pie-avatar/src/index.ts b/packages/components/pie-avatar/src/index.ts index a080aab10c..e39aef30d7 100644 --- a/packages/components/pie-avatar/src/index.ts +++ b/packages/components/pie-avatar/src/index.ts @@ -32,108 +32,20 @@ export class PieAvatar extends RtlMixin(PieElement) implements AvatarProps { @property({ type: String }) public src: AvatarProps['src']; - /** - * Attempts to extract initials from the label string. - * If the label is not provided or is invalid, it returns null. - * - * @private - */ - private getInitials (name: string): Initials | null { - try { - if (!name || typeof name !== 'string' || name.trim().length === 0) { - return null; - } - - const nameSplit: string[] = name.trim().replace(/-/g, ' ').split(/\s+/); // [Ada, Lovelace] - const initials: string[] = nameSplit.slice(0, 2).map((word) => word[0].toUpperCase()); // [A, L] - - if (initials.length === 0) { - return null; - } - - return { - visual: initials.join(''), - screenreader: initials.join(', '), // joins the two words by comma so initials are correctly pronounced by screenreaders - }; - } catch (error) { - return null; - } - } - - /** - * Renders the initials both for visual display and for screen readers. - * - * @private - */ - private renderInitials (initials: Initials): TemplateResult { - return html` - - ${initials.screenreader} - `; - } - - /** - * Renders the user icon. - * - * @private - */ - private renderIcon (): TemplateResult { - return html``; - } - - /** - * Renders an image. - * We assign an empty string to the alt attribute for a11y clarity as it explicitly declares the image as decorative - * - * @private - */ - private renderImage (imgSrc: string): TemplateResult { - return html``; - } - - /** - * Renders the inner content of the avatar such as initials, an icon or an image. - * It is a getter because the value is computed based on properties - * - * @private - */ - private get avatarContent (): TemplateResult { - if (this.src) { - return this.renderImage(this.src); - } - - if (this.label) { - const initials = this.getInitials(this.label); - if (initials) { - return this.renderInitials(initials); - } - } - - return this.renderIcon(); - } - - /** - * Renders the avatar wrapper element based on the `tag` property. - * Can be a `button`, `a` or a `div`. - * - * @private - */ - private renderAvatarWrapper (content: TemplateResult): TemplateResult { - const { tag } = this; - - if (tag === 'button') { - return html``; - } - - if (tag === 'a') { - return html`${content}`; + @property({ type: String }) + public type: AvatarProps['type']; + + private getTypeElement () { + switch (this.type) { + case 'checkbox': + return html`

`; + default: + return html``; } - - return html`
${content}
`; } render () { - return this.renderAvatarWrapper(this.avatarContent); + return html``; } // Renders a `CSSResult` generated from SCSS by Vite diff --git a/packages/components/pie-list/src/defs.ts b/packages/components/pie-list/src/defs.ts index 3119266820..02b66346db 100644 --- a/packages/components/pie-list/src/defs.ts +++ b/packages/components/pie-list/src/defs.ts @@ -1,3 +1,21 @@ -// TODO - please remove the eslint disable comment below when you add props to this interface -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ListProps {} +import { type ComponentDefaultProps } from '@justeattakeaway/pie-webc-core'; + +export const listTypes = ['list', 'interactive'] as const; + +export type ListType = typeof listTypes[number]; + +export interface ListProps { + /** + * The type of the list. + * `list` for a standard list, `interactive` for a list with interactive items. + * + * @default "list" + */ + type?: ListType; +} + +export type DefaultProps = ComponentDefaultProps; + +export const defaultProps: DefaultProps = { + type: 'list', +}; diff --git a/packages/components/pie-list/src/index.ts b/packages/components/pie-list/src/index.ts index 504c5e822c..4b0911a6fa 100644 --- a/packages/components/pie-list/src/index.ts +++ b/packages/components/pie-list/src/index.ts @@ -1,9 +1,10 @@ import { html, unsafeCSS } from 'lit'; +import { property, queryAssignedElements } from 'lit/decorators.js'; import { PieElement } from '@justeattakeaway/pie-webc-core/src/internals/PieElement'; import { RtlMixin, safeCustomElement } from '@justeattakeaway/pie-webc-core'; import styles from './list.scss?inline'; -import { type ListProps } from './defs'; +import { type ListProps, defaultProps } from './defs'; // Valid values available to consumers export * from './defs'; @@ -15,8 +16,36 @@ const componentSelector = 'pie-list'; */ @safeCustomElement('pie-list') export class PieList extends RtlMixin(PieElement) implements ListProps { + @property({ type: String }) + public type = defaultProps.type; + + @queryAssignedElements({ flatten: true }) + private _slottedChildren!: HTMLElement[]; + + private _updateRoles () { + if (this.type === 'list') { + this.role = 'list'; + this._slottedChildren.forEach((child) => { + child.setAttribute('role', 'listitem'); + }); + } else { + this.role = null; + this._slottedChildren.forEach((child) => { + child.removeAttribute('role'); + }); + } + } + + updated () { + this._updateRoles(); + } + + private _handleSlotChange () { + this._updateRoles(); + } + render () { - return html`

Hello world!

`; + return html``; } // Renders a `CSSResult` generated from SCSS by Vite diff --git a/packages/components/pie-list/src/list.scss b/packages/components/pie-list/src/list.scss index 649ce446fd..d374e0770c 100644 --- a/packages/components/pie-list/src/list.scss +++ b/packages/components/pie-list/src/list.scss @@ -2,4 +2,10 @@ :host { display: block; + + ::slotted(*:nth-last-child(n+2)) { + // will need an isCompact prop reflected so we can assign a diff value here + padding-block-end: calc(var(--dt-spacing-b) - 1px); + border-bottom: 1px solid var(--dt-color-divider-default); + } } diff --git a/packages/components/pie-list/src/pie-list-item/defs.ts b/packages/components/pie-list/src/pie-list-item/defs.ts index e92dad87f4..9ea52edda9 100644 --- a/packages/components/pie-list/src/pie-list-item/defs.ts +++ b/packages/components/pie-list/src/pie-list-item/defs.ts @@ -1,3 +1,25 @@ -// TODO - please remove the eslint disable comment below when you add props to this interface -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface ListItemProps {} +export interface ListItemProps { + /** + * The primary text content of the list item. + */ + primaryText?: string; + + /** + * The secondary text content of the list item. + */ + secondaryText?: string; + + /** + * When true, applies bold styling to the primary text. + * + * @default false + */ + isBold?: boolean; + + /** + * When true, applies compact styling to the list item. + * + * @default false + */ + isCompact?: boolean; +} diff --git a/packages/components/pie-list/src/pie-list-item/index.ts b/packages/components/pie-list/src/pie-list-item/index.ts index ce41edc8bd..14545f2c9d 100644 --- a/packages/components/pie-list/src/pie-list-item/index.ts +++ b/packages/components/pie-list/src/pie-list-item/index.ts @@ -1,5 +1,11 @@ -import { LitElement } from 'lit'; +import { + LitElement, html, unsafeCSS, nothing, +} from 'lit'; +import { property } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; import { safeCustomElement } from '@justeattakeaway/pie-webc-core'; + +import styles from './list-item.scss?inline'; import { type ListItemProps } from './defs'; const componentSelector = 'pie-list-item'; @@ -9,7 +15,31 @@ const componentSelector = 'pie-list-item'; */ @safeCustomElement('pie-list-item') export class PieListItem extends LitElement implements ListItemProps { - // component logic + @property({ type: String, attribute: 'primary-text' }) + public primaryText?: string; + + @property({ type: String, attribute: 'secondary-text' }) + public secondaryText?: string; + + @property({ type: Boolean, attribute: 'is-bold' }) + public isBold = false; + + @property({ type: Boolean, reflect: true, attribute: 'is-compact' }) + public isCompact = false; + + render () { + return html` + +
+ + +
+ + `; + } + + // Renders a `CSSResult` generated from SCSS by Vite + static styles = unsafeCSS(styles); } declare global { diff --git a/packages/components/pie-list/src/pie-list-item/list-item.scss b/packages/components/pie-list/src/pie-list-item/list-item.scss index 6ffaedad64..2319a15a5a 100644 --- a/packages/components/pie-list/src/pie-list-item/list-item.scss +++ b/packages/components/pie-list/src/pie-list-item/list-item.scss @@ -1 +1,88 @@ @use '@justeattakeaway/pie-css/scss' as p; + +:host { + // Expose for consumers to override + --list-item-inline-padding: var(--dt-spacing-d); + + display: flex; + flex-direction: row; + align-items: center; + position: relative; + min-height: 56px; + padding-inline-start: var(--list-item-inline-padding-override, var(--list-item-inline-padding)); + padding-inline-end: var(--list-item-inline-padding-override, var(--list-item-inline-padding)); + + ::slotted([slot="leading"]) { + margin-inline-start: auto; + margin-inline-end: var(--dt-spacing-c); + color: var(--dt-color-content-subdued); + } + + // whatever the size would actually be + ::slotted(.c-pieIcon) { + --icon-size-override: 21px + } + + // once we sort padding + // ::slotted(input[slot="leading"]) { + // align-self: flex-start; + // } + + ::slotted([slot="text"]) { + margin: 0; + padding: 0; + } + + ::slotted(label):after { + content: ''; + position: absolute; + inset: 0; + } + + // Primary text + ::slotted([slot="primaryText"]) { + color: var(--dt-color-content-default); + @include p.font-theme('font-body-l'); + } + + // Secondary text + ::slotted([slot="secondaryText"]) { + color: var(--dt-color-content-subdued); + @include p.font-theme('font-body-s'); + } + + ::slotted([slot="trailing"]) { + margin-inline-end: auto; + margin-inline-start: var(--dt-spacing-d); + color: var(--dt-color-content-subdued); + // For meta text + @include p.font-theme('font-body-s'); + } + + .c-listItem-text { + flex: 1; + display: flex; + flex-direction: column; + margin-inline-end: auto; + } +} + +// compact +:host([is-compact]) { + min-height: 48px; +} + +// primary + secondary +:host([secondary-text]) { + min-height: 76px; +} + +// bold - BUG: when no leading slot it targets the wrong text +:host([is-bold]) ::slotted([slot="primaryText"]) { + @include p.font-theme('font-body-strong-l'); +} + +// Radio slot styles +@include p.radio-slotted-input-base('input[type="radio"]'); +@include p.radio-slotted-interactive-state('input[type="radio"]', 'dt-color-interactive-brand'); +@include p.radio-slotted-animations('input[type="radio"]'); diff --git a/packages/tools/pie-css/scss/mixins/components/_radio.scss b/packages/tools/pie-css/scss/mixins/components/_radio.scss index d4bed55954..5ba31356ee 100644 --- a/packages/tools/pie-css/scss/mixins/components/_radio.scss +++ b/packages/tools/pie-css/scss/mixins/components/_radio.scss @@ -193,3 +193,206 @@ } } } + +// ============================================================================= +// SLOTTED VARIANTS +// +// When styling a slotted inside a shadow DOM host, +// pseudo-classes (:checked, :disabled, :focus-visible, etc.) must go INSIDE +// the ::slotted() functional notation, while pseudo-elements (::before, ::after) +// must go AFTER it. +// +// Standard mixins produce: `&:checked::after { ... }` → invalid when nested +// inside `::slotted(...)`. +// +// Slotted mixins produce: `::slotted(selector:checked)::after { ... }` → valid. +// +// Usage: +// @include radio-slotted-input-base('input[type="radio"]'); +// @include radio-slotted-interactive-state('input[type="radio"]', 'dt-color-interactive-brand'); +// @include radio-slotted-animations('input[type="radio"]'); +// @include radio-slotted-error('input[type="radio"]'); +// ============================================================================= + +/// Slotted variant of radio-input-base. +/// Applies base styles, pseudo-elements, and state styles to a slotted radio input. +/// @param {String} $selector - The selector to place inside ::slotted() (e.g. 'input[type="radio"]') +/// @example scss - Basic usage +/// :host { +/// @include radio-slotted-input-base('input[type="radio"]'); +/// } +@mixin radio-slotted-input-base($selector) { + // Base element styles + ::slotted(#{$selector}) { + // CSS custom properties for theming + --radio-dot-bg-color: var(--dt-color-content-interactive-primary); + --radio-bg-color: transparent; + --radio-bg-color--checked: var(--dt-color-interactive-brand); + --radio-border-color: var(--dt-color-border-form); + --radio-size: 24px; + --radio-dot-size: 8px; + --radio-cursor: pointer; + --radio-motion-easing: var(--dt-motion-easing-persistent-functional); + --radio-border-width: 1px; + + // Input element base styles + appearance: none; + display: inline-block; + position: relative; + inline-size: var(--radio-size); + block-size: var(--radio-size); + border: var(--radio-border-width) solid var(--radio-border-color); + border-radius: 50%; + margin: 0; + cursor: var(--radio-cursor); + background-color: var(--radio-bg-color); + flex-shrink: 0; + } + + // The filled circle before checking the radio + ::slotted(#{$selector})::before { + --circle-size: calc(var(--radio-border-width) * -1); + + content: ''; + display: block; + inset: var(--circle-size); + border-radius: inherit; + background-color: var(--radio-bg-color--checked); + position: absolute; + transform: scale(0); + } + + // The dot in the middle before checking the radio + ::slotted(#{$selector})::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: var(--radio-dot-size); + height: var(--radio-dot-size); + background-color: var(--radio-dot-bg-color); + border-radius: 50%; + transform: translate(-50%, -50%) scale(0); + } + + // Checked state - filled circle + ::slotted(#{$selector}:checked)::before { + transform: scale(1); + } + + // Checked state - dot + ::slotted(#{$selector}:checked)::after { + transform: translate(-50%, -50%) scale(1); + } + + // Focus state + ::slotted(#{$selector}:focus-visible) { + @include focus; + } + + // Disabled state + ::slotted(#{$selector}:disabled) { + --radio-bg-color: var(--dt-color-disabled-01); + --radio-border-color: var(--dt-color-border-default); + --radio-cursor: not-allowed; + } + + // Checked + disabled combination + ::slotted(#{$selector}:checked:disabled) { + --radio-dot-bg-color: var(--dt-color-content-disabled); + --radio-bg-color--checked: var(--dt-color-disabled-01); + } +} + +/// Slotted variant of radio-interactive-state. +/// @param {String} $selector - The selector to place inside ::slotted() +/// @param {String} $bg-color - The design token name for the background color +/// @example scss - Basic usage +/// :host { +/// @include radio-slotted-interactive-state('input[type="radio"]', 'dt-color-interactive-brand'); +/// } +@mixin radio-slotted-interactive-state($selector, $bg-color) { + // Unchecked hover state + ::slotted(#{$selector}:hover:not(:checked, :disabled)) { + --radio-bg-color: hsl(var(--dt-color-black-h), var(--dt-color-black-s), var(--dt-color-black-l), var(--dt-color-hover-01)); + + @supports (background-color: color-mix(in srgb, black, white)) { + --radio-bg-color: color-mix(in srgb, var(--dt-color-hover-01-bg) var(--dt-color-hover-01), transparent); + } + } + + // Unchecked active state + ::slotted(#{$selector}:active:not(:checked, :disabled)) { + --radio-bg-color: hsl(var(--dt-color-black-h), var(--dt-color-black-s), var(--dt-color-black-l), var(--dt-color-active-01)); + + @supports (background-color: color-mix(in srgb, black, white)) { + --radio-bg-color: color-mix(in srgb, var(--dt-color-active-01-bg) var(--dt-color-active-01), transparent); + } + } + + // Checked hover state + ::slotted(#{$selector}:hover:checked:not(:disabled))::before { + --radio-bg-color--checked: hsl(var(--#{$bg-color}-h), var(--#{$bg-color}-s), calc(var(--#{$bg-color}-l) - var(--dt-color-hover-01))); + --radio-border-color: hsl(var(--#{$bg-color}-h), var(--#{$bg-color}-s), calc(var(--#{$bg-color}-l) - var(--dt-color-hover-01))); + + @supports (background-color: color-mix(in srgb, black, white)) { + --radio-bg-color--checked: color-mix(in srgb, var(--dt-color-hover-01-bg) var(--dt-color-hover-01), var(--#{$bg-color})); + } + } + + // Checked active state + ::slotted(#{$selector}:active:checked:not(:disabled))::before { + --radio-bg-color--checked: hsl(var(--#{$bg-color}-h), var(--#{$bg-color}-s), calc(var(--#{$bg-color}-l) - var(--dt-color-active-01))); + --radio-border-color: hsl(var(--#{$bg-color}-h), var(--#{$bg-color}-s), calc(var(--#{$bg-color}-l) - var(--dt-color-active-01))); + + @supports (background-color: color-mix(in srgb, black, white)) { + --radio-bg-color--checked: color-mix(in srgb, var(--dt-color-active-01-bg) var(--dt-color-active-01), var(--#{$bg-color})); + } + } +} + +/// Slotted variant of radio-error. +/// @param {String} $selector - The selector to place inside ::slotted() +/// @example scss - Basic usage +/// :host { +/// @include radio-slotted-error('input[type="radio"]'); +/// } +@mixin radio-slotted-error($selector) { + ::slotted(#{$selector}) { + --radio-bg-color--checked: var(--dt-color-support-error); + --radio-border-color: var(--dt-color-support-error); + } +} + +/// Slotted variant of radio-animations. +/// @param {String} $selector - The selector to place inside ::slotted() +/// @example scss - Basic usage +/// :host { +/// @include radio-slotted-animations('input[type="radio"]'); +/// } +@mixin radio-slotted-animations($selector) { + ::slotted(#{$selector}) { + transition: background-color var(--dt-motion-timing-100) var(--radio-motion-easing); + } + + // Transition for the filled circle + ::slotted(#{$selector}:not(:disabled))::before { + @media (prefers-reduced-motion: no-preference) { + transition: all var(--dt-motion-timing-100) var(--radio-motion-easing); + } + } + + // Transition for the dot when unchecking (scales down at 100ms) + ::slotted(#{$selector}:not(:disabled))::after { + @media (prefers-reduced-motion: no-preference) { + transition: all var(--dt-motion-timing-100) var(--radio-motion-easing); + } + } + + // Transition for the dot when checking (scales up at 150ms for emphasis) + ::slotted(#{$selector}:not(:disabled):checked)::after { + @media (prefers-reduced-motion: no-preference) { + transition: all var(--dt-motion-timing-150) var(--radio-motion-easing); + } + } +}