From 396470a235c8ad44558de49314bf5288ded7277e Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 27 Mar 2026 06:39:26 -0700 Subject: [PATCH 1/5] feat(badge): add recipe and tokens --- core/src/components.d.ts | 20 +- core/src/components/badge/badge.common.scss | 47 ----- core/src/components/badge/badge.ionic.scss | 18 +- core/src/components/badge/badge.native.scss | 4 +- core/src/components/badge/badge.scss | 223 ++++++++++++++++++++ core/src/components/badge/badge.tsx | 97 +++++---- core/src/components/chip/chip.tsx | 3 +- core/src/themes/ionic/default.tokens.ts | 16 +- core/src/themes/ios/default.tokens.ts | 42 ++-- core/src/themes/md/default.tokens.ts | 203 ++++++++++++++++-- core/src/themes/themes.interfaces.ts | 2 + core/src/utils/theme.ts | 62 +++++- 12 files changed, 569 insertions(+), 168 deletions(-) delete mode 100644 core/src/components/badge/badge.common.scss create mode 100644 core/src/components/badge/badge.scss diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 7769ddba3af..ac625765ddd 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -467,7 +467,7 @@ export namespace Components { */ "color"?: Color; /** - * Set to `"bold"` for a badge with vibrant, bold colors or to `"subtle"` for a badge with muted, subtle colors. Only applies to the `ionic` theme. + * Set to `"bold"` for a badge with vibrant, bold colors or to `"subtle"` for a badge with muted, subtle colors. Defaults to `"bold"` if both the hue property and theme config are unset. */ "hue"?: 'bold' | 'subtle'; /** @@ -475,11 +475,11 @@ export namespace Components { */ "mode"?: "ios" | "md"; /** - * Set to `"rectangular"` for non-rounded corners. Set to `"soft"` for slightly rounded corners. Set to `"round"` for fully rounded corners. Defaults to `"round"` for the `ionic` theme, undefined for all other themes. + * Set to `"crisp"` for a badge with minimally rounded corners, `"soft"` for a badge with slightly rounded corners, `"round"` for a badge with fully rounded corners, or `"rectangular"` for a badge without rounded corners. Defaults to `"soft"` if both the shape property and theme config are unset. */ - "shape"?: 'soft' | 'round | rectangular'; + "shape"?: 'crisp' | 'soft' | 'round' | 'rectangular'; /** - * Set to `"small"` for a small badge. Set to `"medium"` for a medium badge. Set to `"large"` for a large badge, when it is empty (no text or icon). Defaults to `"small"` for the `ionic` theme, undefined for all other themes. + * Set to `"small"` for a smaller size. Set to `"medium"` for a medium size. Set to `"large"` for a larger size. Defaults to `"small"` if both the size property and theme config are unset. */ "size"?: 'small' | 'medium' | 'large'; /** @@ -896,7 +896,7 @@ export namespace Components { */ "shape"?: 'soft' | 'round' | 'rectangular'; /** - * Set to `"small"` for a chip with less height and padding. Defaults to `"large"` if both the size property and theme config are unset. + * Set to `"small"` for a chip with less height and padding, or `"large"` for a chip with more height and padding. Defaults to `"large"` if both the size property and theme config are unset. */ "size"?: 'small' | 'large'; } @@ -6402,7 +6402,7 @@ declare namespace LocalJSX { */ "color"?: Color; /** - * Set to `"bold"` for a badge with vibrant, bold colors or to `"subtle"` for a badge with muted, subtle colors. Only applies to the `ionic` theme. + * Set to `"bold"` for a badge with vibrant, bold colors or to `"subtle"` for a badge with muted, subtle colors. Defaults to `"bold"` if both the hue property and theme config are unset. */ "hue"?: 'bold' | 'subtle'; /** @@ -6410,11 +6410,11 @@ declare namespace LocalJSX { */ "mode"?: "ios" | "md"; /** - * Set to `"rectangular"` for non-rounded corners. Set to `"soft"` for slightly rounded corners. Set to `"round"` for fully rounded corners. Defaults to `"round"` for the `ionic` theme, undefined for all other themes. + * Set to `"crisp"` for a badge with minimally rounded corners, `"soft"` for a badge with slightly rounded corners, `"round"` for a badge with fully rounded corners, or `"rectangular"` for a badge without rounded corners. Defaults to `"soft"` if both the shape property and theme config are unset. */ - "shape"?: 'soft' | 'round | rectangular'; + "shape"?: 'crisp' | 'soft' | 'round' | 'rectangular'; /** - * Set to `"small"` for a small badge. Set to `"medium"` for a medium badge. Set to `"large"` for a large badge, when it is empty (no text or icon). Defaults to `"small"` for the `ionic` theme, undefined for all other themes. + * Set to `"small"` for a smaller size. Set to `"medium"` for a medium size. Set to `"large"` for a larger size. Defaults to `"small"` if both the size property and theme config are unset. */ "size"?: 'small' | 'medium' | 'large'; /** @@ -6866,7 +6866,7 @@ declare namespace LocalJSX { */ "shape"?: 'soft' | 'round' | 'rectangular'; /** - * Set to `"small"` for a chip with less height and padding. Defaults to `"large"` if both the size property and theme config are unset. + * Set to `"small"` for a chip with less height and padding, or `"large"` for a chip with more height and padding. Defaults to `"large"` if both the size property and theme config are unset. */ "size"?: 'small' | 'large'; } diff --git a/core/src/components/badge/badge.common.scss b/core/src/components/badge/badge.common.scss deleted file mode 100644 index 1523886ea5e..00000000000 --- a/core/src/components/badge/badge.common.scss +++ /dev/null @@ -1,47 +0,0 @@ -@use "../../themes/functions.color" as color; -@import "../../themes/mixins"; - -// Badge -// -------------------------------------------------- - -:host { - /** - * @prop --background: Background of the badge - * @prop --color: Text color of the badge - * - * @prop --padding-top: Top padding of the badge - * @prop --padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of the badge - * @prop --padding-bottom: Bottom padding of the badge - * @prop --padding-start: Left padding if direction is left-to-right, and right padding if direction is right-to-left of the badge - */ - @include font-smoothing(); - @include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start)); - - display: inline-block; - - background: var(--background); - color: var(--color); - - text-align: center; - - white-space: nowrap; - - contain: content; - vertical-align: baseline; -} - -// Badge (hint) -// -------------------------------------------------- - -:host([vertical]:not(.in-tab-button)) { - @include position(null, 0, null, null); - position: absolute; -} - -:host(:not(.in-tab-button)[vertical].badge-vertical-top) { - top: 0; -} - -:host(:not(.in-tab-button)[vertical].badge-vertical-bottom) { - bottom: 0; -} diff --git a/core/src/components/badge/badge.ionic.scss b/core/src/components/badge/badge.ionic.scss index 4342c2b51d3..b8e36943689 100644 --- a/core/src/components/badge/badge.ionic.scss +++ b/core/src/components/badge/badge.ionic.scss @@ -1,5 +1,5 @@ @use "../../themes/ionic/ionic.globals.scss" as globals; -@use "./badge.common"; +@use "./badge"; // Ionic Badge // -------------------------------------------------- @@ -51,21 +51,22 @@ /* Soft Badge */ :host(.badge-soft) { - @include globals.border-radius(globals.$ion-soft-xs); + @include globals.border-radius(globals.$ion-soft-xs); // 8px } +// TODO: add example that this needs to be done by passing props of small size ane crisp shape :host(.badge-small.badge-soft) { - @include globals.border-radius(globals.$ion-soft-2xs); + @include globals.border-radius(globals.$ion-soft-2xs); // 4px } /* Round Badge */ :host(.badge-round) { - @include globals.border-radius(globals.$ion-round-sm); + @include globals.border-radius(globals.$ion-round-sm); // 999px } /* Rectangular Badge */ :host(.badge-rectangular) { - @include globals.border-radius(globals.$ion-rectangular-sm); + @include globals.border-radius(globals.$ion-rectangular-sm); // 0 } // Badge Sizes @@ -77,7 +78,7 @@ --padding-end: #{globals.$ion-space-050}; min-width: globals.$ion-scale-400; - height: globals.$ion-scale-400; + height: globals.$ion-scale-400; // 16px } :host(.badge-small) ::slotted(ion-icon) { @@ -92,10 +93,10 @@ --padding-start: #{globals.$ion-space-100}; --padding-end: #{globals.$ion-space-100}; - @include globals.typography(globals.$ion-body-md-medium); + @include globals.typography(globals.$ion-body-md-medium); // TODO: Add this min-width: globals.$ion-scale-600; - height: globals.$ion-scale-600; + height: globals.$ion-scale-600; // 24px } :host(.badge-medium) ::slotted(ion-icon), @@ -112,6 +113,7 @@ --padding-end: 0; } +// TODO: don't add since this is an equivalent of size medium so give example :host([vertical]:not(:empty)) { --padding-start: #{globals.$ion-scale-100}; --padding-end: #{globals.$ion-scale-100}; diff --git a/core/src/components/badge/badge.native.scss b/core/src/components/badge/badge.native.scss index 0afa59779d6..6bb0eebf0c9 100644 --- a/core/src/components/badge/badge.native.scss +++ b/core/src/components/badge/badge.native.scss @@ -1,5 +1,5 @@ @import "../../themes/native/native.globals.md"; -@import "./badge.common"; +@import "./badge"; @import "./badge.native.vars"; // Badge @@ -15,7 +15,7 @@ min-width: $badge-min-width; - font-family: $font-family-base; + font-family: $font-family-base; // TODO: Add this font-size: $badge-font-size; font-weight: $badge-font-weight; diff --git a/core/src/components/badge/badge.scss b/core/src/components/badge/badge.scss new file mode 100644 index 00000000000..400cb4d9d05 --- /dev/null +++ b/core/src/components/badge/badge.scss @@ -0,0 +1,223 @@ +@use "../../themes/mixins" as mixins; +@use "../../themes/functions.color" as colors; + +// Badge: Common Styles +// -------------------------------------------------- + +:host { + /** + * @prop --background: Background of the badge + * @prop --color: Text color of the badge + * + * @prop --padding-top: Top padding of the badge + * @prop --padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of the badge + * @prop --padding-bottom: Bottom padding of the badge + * @prop --padding-start: Left padding if direction is left-to-right, and right padding if direction is right-to-left of the badge + */ + @include mixins.font-smoothing(); + + display: var(--ion-badge-display); + + font-size: var(--ion-badge-font-size); + font-weight: var(--ion-badge-font-weight); + + line-height: var(--ion-badge-line-height); + + text-align: center; + + white-space: nowrap; + + contain: content; + vertical-align: baseline; +} + +// Badge: Bold +// --------------------------------------------- + +// Default +:host(.badge-bold) { + background: var(--ion-badge-hue-bold-default-background); + color: var(--ion-badge-hue-bold-default-color); +} + +// Colors +:host(.badge-bold.ion-color) { + background: var(--ion-badge-hue-bold-semantic-default-background); + color: var(--ion-badge-hue-bold-semantic-default-color); +} + +// Badge: Subtle +// --------------------------------------------- + +// Default +:host(.badge-subtle) { + background: var(--ion-badge-hue-subtle-default-background); + color: var(--ion-badge-hue-subtle-default-color); +} + +// Colors +:host(.badge-subtle.ion-color) { + background: var(--ion-badge-hue-subtle-semantic-default-background); + color: var(--ion-badge-hue-subtle-semantic-default-color); +} + +// Badge Shapes +// --------------------------------------------- + +:host(.badge-crisp) { + @include mixins.border-radius(var(--ion-badge-shape-crisp-border-radius)); +} // 4px + +:host(.badge-soft) { + @include mixins.border-radius(var(--ion-badge-shape-soft-border-radius)); +} + +:host(.badge-round) { + @include mixins.border-radius(var(--ion-badge-shape-round-border-radius)); +} + +:host(.badge-rectangular) { + @include mixins.border-radius(var(--ion-badge-shape-rectangular-border-radius)); +} + +// Badge Sizes: Standalone +// --------------------------------------------- + +// Small +:host(.badge-small) { + @include mixins.padding( + var(--ion-badge-size-small-default-padding-top), + var(--ion-badge-size-small-default-padding-end), + var(--ion-badge-size-small-default-padding-bottom), + var(--ion-badge-size-small-default-padding-start) + ); + + min-width: var(--ion-badge-size-small-default-min-width); + height: var(--ion-badge-size-small-default-height); +} + +:host(.badge-small) ::slotted(ion-icon) { + width: var(--ion-badge-size-small-default-icon-width, revert-layer); + height: var(--ion-badge-size-small-default-icon-height, revert-layer); +} + +// Medium +:host(.badge-medium) { + @include mixins.padding( + var(--ion-badge-size-medium-default-padding-top), + var(--ion-badge-size-medium-default-padding-end), + var(--ion-badge-size-medium-default-padding-bottom), + var(--ion-badge-size-medium-default-padding-start) + ); + + min-width: var(--ion-badge-size-medium-default-min-width); + height: var(--ion-badge-size-medium-default-height); +} + +:host(.badge-medium) ::slotted(ion-icon) { + width: var(--ion-badge-size-medium-default-icon-width, revert-layer); + height: var(--ion-badge-size-medium-default-icon-height, revert-layer); +} + +// Large +:host(.badge-large) { + @include mixins.padding( + var(--ion-badge-size-large-default-padding-top), + var(--ion-badge-size-large-default-padding-end), + var(--ion-badge-size-large-default-padding-bottom), + var(--ion-badge-size-large-default-padding-start) + ); + + min-width: var(--ion-badge-size-large-default-min-width); + height: var(--ion-badge-size-large-default-height); +} + +:host(.badge-large) ::slotted(ion-icon) { + width: var(--ion-badge-size-large-default-icon-width, revert-layer); + height: var(--ion-badge-size-large-default-icon-height, revert-layer); +} + +// Badge Sizes: Indicator +// --------------------------------------------- + +:host(.badge-small:empty) { + min-width: var(--ion-badge-size-small-empty-min-width); + height: var(--ion-badge-size-small-empty-height); +} + +/* md */ +:host(.badge-medium:empty) { + min-width: var(--ion-badge-size-medium-empty-min-width); + height: var(--ion-badge-size-medium-empty-height); +} + +/* lg */ +:host(.badge-large:empty) { + min-width: var(--ion-badge-size-large-empty-min-width); + height: var(--ion-badge-size-large-empty-height); +} + +// Empty Badge +// --------------------------------------------- + +:host(:empty) { + @include mixins.padding(0); +} + +// Badge Indicator (within a component) +// --------------------------------------------- + +:host([vertical]:not(.in-tab-button)) { + @include mixins.position(null, 0, null, null); + position: absolute; +} + +:host([vertical]:not(.in-tab-button).badge-vertical-top) { + top: 0; +} + +:host([vertical]:not(.in-tab-button).badge-vertical-bottom) { + bottom: 0; +} + +// In Button +:host(.in-button:not(:empty)) { + min-width: var(--ion-badge-indicator-in-button-default-min-width); + height: var(--ion-badge-indicator-in-button-default-height); + + font-size: var(--ion-badge-indicator-in-button-default-font-size); + + line-height: var(--ion-badge-indicator-in-button-default-line-height); + + ::slotted(ion-icon) { + width: var(--ion-badge-indicator-in-button-default-icon-width, revert-layer); + height: var(--ion-badge-indicator-in-button-default-icon-height, revert-layer); + } +} + +:host(.in-button:not(:empty)) ::slotted(ion-icon) { + width: var(--ion-badge-indicator-in-button-default-icon-width, revert-layer); + height: var(--ion-badge-indicator-in-button-default-icon-height, revert-layer); +} + +// TODO: remove from the tab-button.common +// In Tab Button +:host([vertical].in-tab-button) { + position: var(--ion-badge-indicator-in-tab-button-default-position); +} + +// Slotted Icon +:host([vertical]) ::slotted(ion-icon) { + @include globals.position( + var( + --ion-badge-indicator-icon-position-top, + --ion-badge-indicator-icon-position-end, + --ion-badge-indicator-icon-position-bottom, + --ion-badge-indicator-icon-position-start + ) + ); + + position: absolute; + + transform: translate(-50%, -50%); +} diff --git a/core/src/components/badge/badge.tsx b/core/src/components/badge/badge.tsx index 369c2ad199b..a4e812b1b37 100644 --- a/core/src/components/badge/badge.tsx +++ b/core/src/components/badge/badge.tsx @@ -2,6 +2,7 @@ import type { ComponentInterface } from '@stencil/core'; import { Component, Element, Host, Prop, h } from '@stencil/core'; import { createColorClasses, hostContext } from '@utils/theme'; +import { config } from '../../global/config'; import { getIonTheme } from '../../global/ionic-global'; import type { Color } from '../../interface'; @@ -11,11 +12,7 @@ import type { Color } from '../../interface'; */ @Component({ tag: 'ion-badge', - styleUrls: { - ios: 'badge.ios.scss', - md: 'badge.md.scss', - ionic: 'badge.ionic.scss', - }, + styleUrl: 'badge.scss', shadow: true, }) export class Badge implements ComponentInterface { @@ -32,25 +29,26 @@ export class Badge implements ComponentInterface { * Set to `"bold"` for a badge with vibrant, bold colors or to `"subtle"` for * a badge with muted, subtle colors. * - * Only applies to the `ionic` theme. + * Defaults to `"bold"` if both the hue property and theme config are unset. */ @Prop() hue?: 'bold' | 'subtle'; /** - * Set to `"rectangular"` for non-rounded corners. - * Set to `"soft"` for slightly rounded corners. - * Set to `"round"` for fully rounded corners. + * Set to `"crisp"` for a badge with minimally rounded corners, + * `"soft"` for a badge with slightly rounded corners, + * `"round"` for a badge with fully rounded corners, + * or `"rectangular"` for a badge without rounded corners. * - * Defaults to `"round"` for the `ionic` theme, undefined for all other themes. + * Defaults to `"soft"` if both the shape property and theme config are unset. */ - @Prop() shape?: 'soft' | 'round | rectangular'; + @Prop() shape?: 'crisp' | 'soft' | 'round' | 'rectangular'; /** - * Set to `"small"` for a small badge. - * Set to `"medium"` for a medium badge. - * Set to `"large"` for a large badge, when it is empty (no text or icon). + * Set to `"small"` for a smaller size. + * Set to `"medium"` for a medium size. + * Set to `"large"` for a larger size. * - * Defaults to `"small"` for the `ionic` theme, undefined for all other themes. + * Defaults to `"small"` if both the size property and theme config are unset. */ @Prop() size?: 'small' | 'medium' | 'large'; @@ -60,34 +58,24 @@ export class Badge implements ComponentInterface { */ @Prop() vertical?: 'top' | 'bottom'; - private getShape(): string | undefined { - const theme = getIonTheme(this); - const { shape } = this; - - // TODO(ROU-10777): Remove theme check when shapes are defined for all themes. - if (theme !== 'ionic') { - return undefined; - } - - if (shape === undefined) { - return 'round'; - } + /** + * Gets the badge shape. Uses the `shape` property if set, otherwise + * checks the theme config and falls back to 'soft' if neither is provided. + */ + get shapeValue(): string { + const shapeConfig = config.getObjectValue('IonBadge.shape', 'soft') as string; + const shape = this.shape || shapeConfig; return shape; } - private getSize(): string | undefined { - const theme = getIonTheme(this); - const { size } = this; - - // TODO(FW-6355): Remove theme check when sizes are defined for all themes. - if (theme !== 'ionic') { - return undefined; - } - - if (size === undefined) { - return 'small'; - } + /** + * Gets the badge size. Uses the `size` property if set, otherwise + * checks the theme config and falls back to 'small' if neither is provided. + */ + get sizeValue(): string { + const sizeConfig = config.getObjectValue('IonBadge.size', 'small') as string; + const size = this.size || sizeConfig; return size; } @@ -117,23 +105,32 @@ export class Badge implements ComponentInterface { return 'subtle'; } + /** + * Gets the badge hue. Uses the `hue` property if set, otherwise + * checks the theme config and falls back to 'subtle' if neither is provided. + */ + get hueValue(): string { + const hueConfig = config.getObjectValue('IonBadge.hue', 'bold') as string; + const hue = this.hue || hueConfig; + + return hue; + } + render() { - const hue = this.getHue(); - const shape = this.getShape(); - const size = this.getSize(); const theme = getIonTheme(this); + const { hueValue, shapeValue, sizeValue, color, vertical, el } = this; return ( 2, + [`badge-${hueValue}`]: true, + [`badge-${shapeValue}`]: true, + [`badge-${sizeValue}`]: true, + [`badge-vertical-${vertical}`]: vertical !== undefined, + 'in-button': hostContext('ion-button', el), + 'in-tab-button': hostContext('ion-tab-button', el), + 'long-badge': (el.textContent?.trim().length ?? 0) > 2, })} > diff --git a/core/src/components/chip/chip.tsx b/core/src/components/chip/chip.tsx index 092ce877ff1..07201d3601e 100644 --- a/core/src/components/chip/chip.tsx +++ b/core/src/components/chip/chip.tsx @@ -65,7 +65,8 @@ export class Chip implements ComponentInterface { // TODO(FW-6266): Determine if `medium` size is needed. /** - * Set to `"small"` for a chip with less height and padding. + * Set to `"small"` for a chip with less height and padding, + * or `"large"` for a chip with more height and padding. * * Defaults to `"large"` if both the size property and theme config are unset. */ diff --git a/core/src/themes/ionic/default.tokens.ts b/core/src/themes/ionic/default.tokens.ts index 9fe7932858a..2f4fdb081f2 100644 --- a/core/src/themes/ionic/default.tokens.ts +++ b/core/src/themes/ionic/default.tokens.ts @@ -19,6 +19,12 @@ export const defaultTheme: DefaultTheme = { formHighlight: true, components: { + IonBadge: { + hue: 'subtle', + shape: 'round', + size: 'small', + }, + IonChip: { fill: 'solid', hue: 'subtle', @@ -223,8 +229,8 @@ export const defaultTheme: DefaultTheme = { // Any of the semantic colors like primary, secondary, etc. semantic: { default: { - background: currentColor('base', null, true), - color: currentColor('contrast', null, true), + background: currentColor('base', { subtle: true }), + color: currentColor('contrast', { subtle: true }), }, }, }, @@ -245,11 +251,11 @@ export const defaultTheme: DefaultTheme = { // Any of the semantic colors like primary, secondary, etc. semantic: { default: { - background: currentColor('base', null, true), - color: currentColor('contrast', null, true), + background: currentColor('base', { subtle: true }), + color: currentColor('contrast', { subtle: true }), border: { - color: currentColor('shade', null, true), + color: currentColor('shade', { subtle: true }), style: 'solid', width: '1px', }, diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts index e36d7e3e007..f0a75218831 100644 --- a/core/src/themes/ios/default.tokens.ts +++ b/core/src/themes/ios/default.tokens.ts @@ -26,6 +26,12 @@ export const defaultTheme: DefaultTheme = { config: { components: { + IonBadge: { + hue: 'bold', + shape: 'soft', + size: 'small', + }, + IonChip: { fill: 'solid', hue: 'bold', @@ -189,20 +195,20 @@ export const defaultTheme: DefaultTheme = { // Any of the semantic colors like primary, secondary, etc. semantic: { default: { - background: currentColor('base', 0.08), + background: currentColor('base', { alpha: 0.08 }), color: currentColor('shade'), }, hover: { - background: currentColor('base', 0.12), + background: currentColor('base', { alpha: 0.12 }), }, focus: { - background: currentColor('base', 0.12), + background: currentColor('base', { alpha: 0.12 }), }, activated: { - background: currentColor('base', 0.16), + background: currentColor('base', { alpha: 0.16 }), }, }, }, @@ -239,22 +245,22 @@ export const defaultTheme: DefaultTheme = { color: currentColor('shade'), border: { - color: currentColor('base', 0.32), + color: currentColor('base', { alpha: 0.32 }), style: 'solid', width: 'var(--ion-border-width-xxs)', }, }, hover: { - background: currentColor('base', 0.12), + background: currentColor('base', { alpha: 0.12 }), }, focus: { - background: currentColor('base', 0.12), + background: currentColor('base', { alpha: 0.12 }), }, activated: { - background: currentColor('base', 0.16), + background: currentColor('base', { alpha: 0.16 }), }, }, }, @@ -283,20 +289,20 @@ export const defaultTheme: DefaultTheme = { // Any of the semantic colors like primary, secondary, etc. semantic: { default: { - background: currentColor('base', null, true), - color: currentColor('contrast', null, true), + background: currentColor('base', { subtle: true }), + color: currentColor('contrast', { subtle: true }), }, hover: { - background: currentColor('base', 0.6, true), + background: currentColor('base', { alpha: 0.6, subtle: true }), }, focus: { - background: currentColor('base', 0.6, true), + background: currentColor('base', { alpha: 0.6, subtle: true }), }, activated: { - background: currentColor('base', 0.8, true), + background: currentColor('base', { alpha: 0.8, subtle: true }), }, }, }, @@ -330,25 +336,25 @@ export const defaultTheme: DefaultTheme = { semantic: { default: { background: 'transparent', - color: currentColor('contrast', null, true), + color: currentColor('contrast', { subtle: true }), border: { - color: currentColor('base', 0.12), + color: currentColor('base', { alpha: 0.12 }), style: 'solid', width: 'var(--ion-border-width-xxs)', }, }, hover: { - background: currentColor('base', 0.6, true), + background: currentColor('base', { alpha: 0.6, subtle: true }), }, focus: { - background: currentColor('base', 0.6, true), + background: currentColor('base', { alpha: 0.6, subtle: true }), }, activated: { - background: currentColor('base', 0.8, true), + background: currentColor('base', { alpha: 0.8, subtle: true }), }, }, }, diff --git a/core/src/themes/md/default.tokens.ts b/core/src/themes/md/default.tokens.ts index 80141bb7f55..dc1e460504f 100644 --- a/core/src/themes/md/default.tokens.ts +++ b/core/src/themes/md/default.tokens.ts @@ -1,4 +1,4 @@ -import { rgba, currentColor } from '../../utils/theme'; +import { rgba, currentColor, ionColor } from '../../utils/theme'; import { defaultTheme as baseDefaultTheme } from '../base/default.tokens'; import type { DefaultTheme } from '../themes.interfaces'; @@ -29,6 +29,12 @@ export const defaultTheme: DefaultTheme = { rippleEffect: true, components: { + IonBadge: { + hue: 'bold', + shape: 'crisp', + size: 'small', + }, + IonChip: { fill: 'solid', hue: 'bold', @@ -101,6 +107,165 @@ export const defaultTheme: DefaultTheme = { }, components: { + IonBadge: { + display: 'inline-block', + + font: { + // size: dynamicFont(13), // TODO: uncomment this when dynamic font is implemented + size: `13px`, // TODO: remove this when dynamic font is implemented + weight: 'var(--ion-font-weight-bold)', + }, + + line: { + height: '1', + }, + + // Hues + hue: { + bold: { + default: { + background: ionColor('primary', 'base'), + color: ionColor('primary', 'contrast'), + }, + + semantic: { + default: { + background: currentColor('base'), + color: currentColor('contrast'), + }, + }, + }, + + subtle: { + default: { + background: ionColor('primary', 'base', { subtle: true }), + color: ionColor('primary', 'contrast', { subtle: true }), + }, + + semantic: { + default: { + background: currentColor('base', { subtle: true }), + color: currentColor('contrast', { subtle: true }), + }, + }, + }, + }, + + // Shapes + shape: { + crisp: { + border: { + radius: 'var(--ion-radii-sm)', + }, + }, + + soft: { + border: { + radius: 'var(--ion-radii-md)', + }, + }, + + round: { + border: { + radius: 'var(--ion-radii-xxxxl)', + }, + }, + + rectangular: { + border: { + radius: 'var(--ion-radii-xxxxs)', + }, + }, + }, + + // Sizes + size: { + small: { + default: { + min: { + width: 'var(--ion-scaling-250)', + }, + + padding: { + top: 'var(--ion-spacing-75)', + end: 'var(--ion-spacing-xxs)', + bottom: 'var(--ion-spacing-xxs)', + start: 'var(--ion-spacing-xxs)', + }, + }, + }, + + medium: { + default: { + min: { + width: 'var(--ion-scaling-xxxs)', + }, + + padding: { + top: 'var(--ion-spacing-xs)', + end: 'var(--ion-spacing-xs)', + bottom: 'var(--ion-spacing-xs)', + start: 'var(--ion-spacing-xs)', + }, + }, + }, + + large: { + default: { + min: { + width: 'var(--ion-scaling-xxxs)', + }, + + padding: { + top: 'var(--ion-spacing-sm)', + end: 'var(--ion-spacing-sm)', + bottom: 'var(--ion-spacing-sm)', + start: 'var(--ion-spacing-sm)', + }, + }, + }, + }, + + indicator: { + in: { + button: { + default: { + height: 'var(--ion-scaling-xxxs)', + + min: { + width: 'var(--ion-scaling-xxxs)', + }, + + line: { + height: 'var(--ion-scaling-xxs)', + }, + + font: { + size: `${(12 / fontSizes.root).toFixed(2)}rem`, + }, + + icon: { + width: 'var(--ion-scaling-xxxxs)', + height: 'var(--ion-scaling-xxxxs)', + }, + }, + }, + + tab: { + button: { + default: { + position: 'absolute', + }, + }, + }, + }, + + icon: { + position: {}, + }, + }, + }, + IonChip: { margin: { top: 'var(--ion-spacing-xxs)', @@ -187,20 +352,20 @@ export const defaultTheme: DefaultTheme = { // Any of the semantic colors like primary, secondary, etc. semantic: { default: { - background: currentColor('base', 0.08), + background: currentColor('base', { alpha: 0.08 }), color: currentColor('shade'), }, hover: { - background: currentColor('base', 0.12), + background: currentColor('base', { alpha: 0.12 }), }, focus: { - background: currentColor('base', 0.12), + background: currentColor('base', { alpha: 0.12 }), }, activated: { - background: currentColor('base', 0.16), + background: currentColor('base', { alpha: 0.16 }), }, }, }, @@ -236,22 +401,22 @@ export const defaultTheme: DefaultTheme = { color: currentColor('shade'), border: { - color: currentColor('base', 0.32), + color: currentColor('base', { alpha: 0.32 }), style: 'solid', width: 'var(--ion-border-width-xxxs)', }, }, hover: { - background: currentColor('base', 0.12), + background: currentColor('base', { alpha: 0.12 }), }, focus: { - background: currentColor('base', 0.12), + background: currentColor('base', { alpha: 0.12 }), }, activated: { - background: currentColor('base', 0.16), + background: currentColor('base', { alpha: 0.16 }), }, }, }, @@ -280,20 +445,20 @@ export const defaultTheme: DefaultTheme = { // Any of the semantic colors like primary, secondary, etc. semantic: { default: { - background: currentColor('base', null, true), - color: currentColor('contrast', null, true), + background: currentColor('base', { subtle: true }), + color: currentColor('contrast', { subtle: true }), }, hover: { - background: currentColor('base', 0.6, true), + background: currentColor('base', { alpha: 0.6, subtle: true }), }, focus: { - background: currentColor('base', 0.6, true), + background: currentColor('base', { alpha: 0.6, subtle: true }), }, activated: { - background: currentColor('base', 0.8, true), + background: currentColor('base', { alpha: 0.8, subtle: true }), }, }, }, @@ -327,25 +492,25 @@ export const defaultTheme: DefaultTheme = { semantic: { default: { background: 'transparent', - color: currentColor('contrast', null, true), + color: currentColor('contrast', { subtle: true }), border: { - color: currentColor('base', 0.12), + color: currentColor('base', { alpha: 0.12 }), style: 'solid', width: 'var(--ion-border-width-xxxs)', }, }, hover: { - background: currentColor('base', 0.6, true), + background: currentColor('base', { alpha: 0.6, subtle: true }), }, focus: { - background: currentColor('base', 0.6, true), + background: currentColor('base', { alpha: 0.6, subtle: true }), }, activated: { - background: currentColor('base', 0.8, true), + background: currentColor('base', { alpha: 0.8, subtle: true }), }, }, }, diff --git a/core/src/themes/themes.interfaces.ts b/core/src/themes/themes.interfaces.ts index 5cbf366e37b..07b460bff74 100644 --- a/core/src/themes/themes.interfaces.ts +++ b/core/src/themes/themes.interfaces.ts @@ -245,6 +245,7 @@ export type BaseTheme = { export type IonicConfig = IonicGlobalConfig & { components?: { + IonBadge?: any; IonChip?: IonChipConfig; IonSpinner?: IonSpinnerConfig; }; @@ -283,6 +284,7 @@ export type DefaultTheme = BaseTheme & { }; type Components = { + IonBadge?: any; IonChip?: IonChipRecipe; IonSpinner?: IonSpinnerRecipe; diff --git a/core/src/utils/theme.ts b/core/src/utils/theme.ts index 3653610250e..9d8344f1a75 100644 --- a/core/src/utils/theme.ts +++ b/core/src/utils/theme.ts @@ -594,6 +594,11 @@ export function rgba(colorRgb: string, alpha: number | string): string { return `rgba(${colorRgb}, ${alpha})`; } +interface CurrentColorOptions { + alpha?: number | string | null; + subtle?: boolean; +} + /** * Mimics the Ionic Framework `current-color` function logic to construct CSS color values. * @@ -603,7 +608,9 @@ export function rgba(colorRgb: string, alpha: number | string): string { * @param subtle If true, uses the '--ion-color-subtle-' prefix. * @returns A string containing the CSS value (e.g., 'var(--ion-color-primary)' or 'rgba(var(--ion-color-primary-rgb), 0.16)'). */ -export function currentColor(variation: string, alpha: number | string | null = null, subtle: boolean = false): string { +export function currentColor(variation: string, options: CurrentColorOptions = {}): string { + const { alpha = null, subtle = false } = options; + // 1. Determine the base CSS variable name const variable = subtle ? `--ion-color-subtle-${variation}` : `--ion-color-${variation}`; @@ -611,14 +618,53 @@ export function currentColor(variation: string, alpha: number | string | null = if (alpha === null) { // Corresponds to: @return var(#{$variable}); return `var(${variable})`; - } else { - // 3. Handle the case where alpha is provided - // Corresponds to: @return rgba(var(#{$variable}-rgb), #{$alpha}); + } + + // 3. Handle the case where alpha is provided + // Corresponds to: @return rgba(var(#{$variable}-rgb), #{$alpha}); + + // NOTE: The resulting string uses the CSS variable for the RGB components + // (e.g., '255, 0, 0') and the provided alpha. + return `rgba(var(${variable}-rgb), ${alpha})`; +} + +interface IonColorOptions { + alpha?: number | string | null; + rgb?: boolean; + subtle?: boolean; +} - // NOTE: The resulting string uses the CSS variable for the RGB components - // (e.g., '255, 0, 0') and the provided alpha. - return `rgba(var(${variable}-rgb), ${alpha})`; +export function ionColor(name: string, variation: string, options: IonColorOptions = {}): string { + const { alpha = null, rgb = false, subtle = false } = options; + + // Build base variable name + const base = subtle ? `--ion-color-${name}-subtle` : `--ion-color-${name}`; + const variationSuffix = variation === 'base' ? '' : `-${variation}`; + let variable = `${base}${variationSuffix}`; + + // Build the fallback variable name (only for bold colors) + let fallbackVariable: string | null = null; + + if (!subtle) { + const fallbackBase = `--ion-color-${name}-bold`; + fallbackVariable = `${fallbackBase}${variationSuffix}`; } + + // Handle alpha transparency + if (alpha !== null) { + const rgbVar = `${variable}-rgb`; + const fallbackRgb = fallbackVariable ? `${fallbackVariable}-rgb` : null; + + return fallbackRgb ? `rgba(var(${rgbVar}, var(${fallbackRgb})), ${alpha})` : `rgba(var(${rgbVar}), ${alpha})`; + } + + // Handle RGB variables + if (rgb) { + variable = `${variable}-rgb`; + fallbackVariable = fallbackVariable ? `${fallbackVariable}-rgb` : null; + } + + return fallbackVariable ? `var(${variable}, var(${fallbackVariable}))` : `var(${variable})`; } /** @@ -628,7 +674,7 @@ export function currentColor(variation: string, alpha: number | string | null = * @param min The minimum value * @param val The preferred value * @param max The maximum value - * @returns + * @returns A string containing the CSS clamp() function call (e.g., 'clamp(1rem, 2vw, 3rem)'). */ export function clamp(min: number | string, val: number | string, max: number | string): string { return `clamp(${min}, ${val}, ${max})`; From 9a2e0d1094fd53dd452f1ded9165c35c81d4f252 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 27 Mar 2026 14:23:29 -0700 Subject: [PATCH 2/5] feat(badge): more defaults for md --- core/src/components/avatar/avatar.md.scss | 19 -- core/src/components/badge/badge.scss | 41 ++-- .../src/components/badge/test/hint/index.html | 142 +++++++------- core/src/themes/ios/default.tokens.ts | 185 +++++++++++++++++- core/src/themes/md/default.tokens.ts | 24 +++ 5 files changed, 297 insertions(+), 114 deletions(-) diff --git a/core/src/components/avatar/avatar.md.scss b/core/src/components/avatar/avatar.md.scss index cfcd2520280..b7679654770 100644 --- a/core/src/components/avatar/avatar.md.scss +++ b/core/src/components/avatar/avatar.md.scss @@ -11,22 +11,3 @@ width: $avatar-md-width; height: $avatar-md-height; } - -// Avatar Empty Badge (hint) -// -------------------------------------------------- - -::slotted(ion-badge.badge-vertical-top:empty) { - @include globals.transform(translate(-50%, 50%)); -} - -::slotted(ion-badge.badge-vertical-bottom:empty) { - @include globals.transform(translateX(-100%)); -} - -:host ::slotted(ion-badge.badge-vertical-top:not(:empty)) { - @include globals.transform(translate(0, 100%)); -} - -:host ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) { - @include globals.transform(translate(0, -100%)); -} diff --git a/core/src/components/badge/badge.scss b/core/src/components/badge/badge.scss index 400cb4d9d05..ed52b9c32d5 100644 --- a/core/src/components/badge/badge.scss +++ b/core/src/components/badge/badge.scss @@ -180,26 +180,6 @@ bottom: 0; } -// In Button -:host(.in-button:not(:empty)) { - min-width: var(--ion-badge-indicator-in-button-default-min-width); - height: var(--ion-badge-indicator-in-button-default-height); - - font-size: var(--ion-badge-indicator-in-button-default-font-size); - - line-height: var(--ion-badge-indicator-in-button-default-line-height); - - ::slotted(ion-icon) { - width: var(--ion-badge-indicator-in-button-default-icon-width, revert-layer); - height: var(--ion-badge-indicator-in-button-default-icon-height, revert-layer); - } -} - -:host(.in-button:not(:empty)) ::slotted(ion-icon) { - width: var(--ion-badge-indicator-in-button-default-icon-width, revert-layer); - height: var(--ion-badge-indicator-in-button-default-icon-height, revert-layer); -} - // TODO: remove from the tab-button.common // In Tab Button :host([vertical].in-tab-button) { @@ -208,7 +188,7 @@ // Slotted Icon :host([vertical]) ::slotted(ion-icon) { - @include globals.position( + @include mixins.position( var( --ion-badge-indicator-icon-position-top, --ion-badge-indicator-icon-position-end, @@ -217,7 +197,22 @@ ) ); - position: absolute; + position: var(--ion-badge-indicator-icon-position); + + transform: translate(var(--ion-badge-indicator-icon-translate-x), var(--ion-badge-indicator-icon-translate-y)); +} + +// In Button +:host(.in-button:not(:empty)) { + min-width: var(--ion-badge-indicator-in-button-default-min-width); + height: var(--ion-badge-indicator-in-button-default-height); - transform: translate(-50%, -50%); + font-size: var(--ion-badge-indicator-in-button-default-font-size); + + line-height: var(--ion-badge-indicator-in-button-default-line-height); +} + +:host(.in-button:not(:empty)) ::slotted(ion-icon) { + width: var(--ion-badge-indicator-in-button-default-icon-width, revert-layer); + height: var(--ion-badge-indicator-in-button-default-icon-height, revert-layer); } diff --git a/core/src/components/badge/test/hint/index.html b/core/src/components/badge/test/hint/index.html index fba0d6e73ff..a2a417c2e09 100644 --- a/core/src/components/badge/test/hint/index.html +++ b/core/src/components/badge/test/hint/index.html @@ -35,15 +35,15 @@ Badge small - + Badge Medium - + Badge Large - + @@ -55,143 +55,143 @@
- + - + - + - + - + - +
- + - + - + - + - + - +
- 1 + 1 - 1 + 1 - 1 + 1 - 1 + 1 - 1 + 1 - 1 + 1
- 1 + 1 - 1 + 1 - 1 + 1 - 1 + 1 - 1 + 1 - 1 + 1
- + - + - + - + - + - + @@ -199,37 +199,37 @@
- + - + - + - + - + - + @@ -246,13 +246,13 @@ Tab One - 9 + 9 Tab Two - + @@ -260,13 +260,13 @@ Tab Three - 999 + 999 Tab Four - +
@@ -275,13 +275,13 @@ Tab One - 9 + 9 Tab Two - + @@ -289,13 +289,13 @@ Tab Three - 999 + 999 Tab Four - +
@@ -311,13 +311,13 @@ Tab One - 9 + 9 Tab Two - + @@ -325,13 +325,13 @@ Tab Three - 999 + 999 Tab Four - + @@ -340,13 +340,13 @@ Tab One - 9 + 9 Tab Two - + @@ -354,13 +354,13 @@ Tab Three - 999 + 999 Tab Four - + @@ -373,19 +373,19 @@
- + - 1 + 1 - 99+ + 99+ - + @@ -399,19 +399,19 @@
- + - 1 + 1 - 99+ + 99+ - + @@ -424,37 +424,37 @@
- 1 + 1 - 1 + 1 - 1 + 1 - 1 + 1
- 1 + 1 - 1 + 1 - 1 + 1 - 1 + 1
diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts index f0a75218831..d167f1e8f64 100644 --- a/core/src/themes/ios/default.tokens.ts +++ b/core/src/themes/ios/default.tokens.ts @@ -1,4 +1,4 @@ -import { rgba, currentColor, clamp } from '../../utils/theme'; +import { rgba, currentColor, clamp, ionColor } from '../../utils/theme'; import { defaultTheme as baseDefaultTheme } from '../base/default.tokens'; import type { DefaultTheme } from '../themes.interfaces'; @@ -104,6 +104,189 @@ export const defaultTheme: DefaultTheme = { }, components: { + IonBadge: { + display: 'inline-block', + + font: { + // size: dynamicFont(13), // TODO: uncomment this when dynamic font is implemented + size: `13px`, // TODO: remove this when dynamic font is implemented + weight: 'var(--ion-font-weight-bold)', + }, + + line: { + height: '1', + }, + + // Hues + hue: { + bold: { + default: { + background: ionColor('primary', 'base'), + color: ionColor('primary', 'contrast'), + }, + + semantic: { + default: { + background: currentColor('base'), + color: currentColor('contrast'), + }, + }, + }, + + subtle: { + default: { + background: ionColor('primary', 'base', { subtle: true }), + color: ionColor('primary', 'contrast', { subtle: true }), + }, + + semantic: { + default: { + background: currentColor('base', { subtle: true }), + color: currentColor('contrast', { subtle: true }), + }, + }, + }, + }, + + // Shapes + shape: { + crisp: { + border: { + radius: 'var(--ion-radii-sm)', + }, + }, + + soft: { + border: { + radius: 'var(--ion-radii-md)', + }, + }, + + round: { + border: { + radius: 'var(--ion-radii-xxxxl)', + }, + }, + + rectangular: { + border: { + radius: 'var(--ion-radii-xxxxs)', + }, + }, + }, + + // Sizes + size: { + small: { + default: { + min: { + width: 'var(--ion-scaling-250)', + }, + + padding: { + top: 'var(--ion-spacing-75)', + end: 'var(--ion-spacing-xxs)', + bottom: 'var(--ion-spacing-xxs)', + start: 'var(--ion-spacing-xxs)', + }, + }, + + empty: { + height: 'var(--ion-scaling-150)', + + min: { + width: 'var(--ion-scaling-150)', + }, + }, + }, + + medium: { + default: { + min: { + width: 'var(--ion-scaling-xxxs)', + }, + + padding: { + top: 'var(--ion-spacing-xs)', + end: 'var(--ion-spacing-xs)', + bottom: 'var(--ion-spacing-xs)', + start: 'var(--ion-spacing-xs)', + }, + }, + + empty: { + height: 'var(--ion-scaling-250)', + + min: { + width: 'var(--ion-scaling-250)', + }, + }, + }, + + large: { + default: { + min: { + width: 'var(--ion-scaling-xxxs)', + }, + + padding: { + top: 'var(--ion-spacing-sm)', + end: 'var(--ion-spacing-sm)', + bottom: 'var(--ion-spacing-sm)', + start: 'var(--ion-spacing-sm)', + }, + }, + + empty: { + height: 'var(--ion-scaling-350)', + + min: { + width: 'var(--ion-scaling-350)', + }, + }, + }, + }, + + indicator: { + in: { + button: { + default: { + height: 'var(--ion-scaling-xxxs)', + + min: { + width: 'var(--ion-scaling-xxxs)', + }, + + line: { + height: 'var(--ion-scaling-xxs)', + }, + + font: { + size: `${(12 / fontSizes.root).toFixed(2)}rem`, + }, + + icon: { + width: 'var(--ion-scaling-xxxxs)', + height: 'var(--ion-scaling-xxxxs)', + }, + }, + }, + + tab: { + button: { + default: { + position: 'absolute', + }, + }, + }, + }, + + icon: { + position: {}, + }, + }, + }, + IonChip: { margin: { top: 'var(--ion-spacing-xxs)', diff --git a/core/src/themes/md/default.tokens.ts b/core/src/themes/md/default.tokens.ts index dc1e460504f..fca28e3600c 100644 --- a/core/src/themes/md/default.tokens.ts +++ b/core/src/themes/md/default.tokens.ts @@ -193,6 +193,14 @@ export const defaultTheme: DefaultTheme = { start: 'var(--ion-spacing-xxs)', }, }, + + empty: { + height: 'var(--ion-scaling-150)', + + min: { + width: 'var(--ion-scaling-150)', + }, + }, }, medium: { @@ -208,6 +216,14 @@ export const defaultTheme: DefaultTheme = { start: 'var(--ion-spacing-xs)', }, }, + + empty: { + height: 'var(--ion-scaling-250)', + + min: { + width: 'var(--ion-scaling-250)', + }, + }, }, large: { @@ -223,6 +239,14 @@ export const defaultTheme: DefaultTheme = { start: 'var(--ion-spacing-sm)', }, }, + + empty: { + height: 'var(--ion-scaling-350)', + + min: { + width: 'var(--ion-scaling-350)', + }, + }, }, }, From a93bc68113a5654d6ce8f6c42fe4584c62141bcd Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 27 Mar 2026 16:18:39 -0700 Subject: [PATCH 3/5] feat(badge): remove crisp --- core/src/components.d.ts | 8 +++--- core/src/components/badge/badge.scss | 4 --- core/src/components/badge/badge.tsx | 5 ++-- .../src/components/badge/test/hint/index.html | 28 +++++++++---------- core/src/themes/ios/default.tokens.ts | 2 +- core/src/themes/md/default.tokens.ts | 10 ++----- 6 files changed, 23 insertions(+), 34 deletions(-) diff --git a/core/src/components.d.ts b/core/src/components.d.ts index ac625765ddd..d5dfdf10d84 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -475,9 +475,9 @@ export namespace Components { */ "mode"?: "ios" | "md"; /** - * Set to `"crisp"` for a badge with minimally rounded corners, `"soft"` for a badge with slightly rounded corners, `"round"` for a badge with fully rounded corners, or `"rectangular"` for a badge without rounded corners. Defaults to `"soft"` if both the shape property and theme config are unset. + * Set to `"soft"` for a badge with slightly rounded corners, `"round"` for a badge with fully rounded corners, or `"rectangular"` for a badge without rounded corners. Defaults to `"soft"` if both the shape property and theme config are unset. */ - "shape"?: 'crisp' | 'soft' | 'round' | 'rectangular'; + "shape"?: 'soft' | 'round' | 'rectangular'; /** * Set to `"small"` for a smaller size. Set to `"medium"` for a medium size. Set to `"large"` for a larger size. Defaults to `"small"` if both the size property and theme config are unset. */ @@ -6410,9 +6410,9 @@ declare namespace LocalJSX { */ "mode"?: "ios" | "md"; /** - * Set to `"crisp"` for a badge with minimally rounded corners, `"soft"` for a badge with slightly rounded corners, `"round"` for a badge with fully rounded corners, or `"rectangular"` for a badge without rounded corners. Defaults to `"soft"` if both the shape property and theme config are unset. + * Set to `"soft"` for a badge with slightly rounded corners, `"round"` for a badge with fully rounded corners, or `"rectangular"` for a badge without rounded corners. Defaults to `"soft"` if both the shape property and theme config are unset. */ - "shape"?: 'crisp' | 'soft' | 'round' | 'rectangular'; + "shape"?: 'soft' | 'round' | 'rectangular'; /** * Set to `"small"` for a smaller size. Set to `"medium"` for a medium size. Set to `"large"` for a larger size. Defaults to `"small"` if both the size property and theme config are unset. */ diff --git a/core/src/components/badge/badge.scss b/core/src/components/badge/badge.scss index ed52b9c32d5..dfe6ecafbd3 100644 --- a/core/src/components/badge/badge.scss +++ b/core/src/components/badge/badge.scss @@ -64,10 +64,6 @@ // Badge Shapes // --------------------------------------------- -:host(.badge-crisp) { - @include mixins.border-radius(var(--ion-badge-shape-crisp-border-radius)); -} // 4px - :host(.badge-soft) { @include mixins.border-radius(var(--ion-badge-shape-soft-border-radius)); } diff --git a/core/src/components/badge/badge.tsx b/core/src/components/badge/badge.tsx index a4e812b1b37..86a280956fe 100644 --- a/core/src/components/badge/badge.tsx +++ b/core/src/components/badge/badge.tsx @@ -34,14 +34,13 @@ export class Badge implements ComponentInterface { @Prop() hue?: 'bold' | 'subtle'; /** - * Set to `"crisp"` for a badge with minimally rounded corners, - * `"soft"` for a badge with slightly rounded corners, + * Set to `"soft"` for a badge with slightly rounded corners, * `"round"` for a badge with fully rounded corners, * or `"rectangular"` for a badge without rounded corners. * * Defaults to `"soft"` if both the shape property and theme config are unset. */ - @Prop() shape?: 'crisp' | 'soft' | 'round' | 'rectangular'; + @Prop() shape?: 'soft' | 'round' | 'rectangular'; /** * Set to `"small"` for a smaller size. diff --git a/core/src/components/badge/test/hint/index.html b/core/src/components/badge/test/hint/index.html index a2a417c2e09..dfd462aa329 100644 --- a/core/src/components/badge/test/hint/index.html +++ b/core/src/components/badge/test/hint/index.html @@ -377,15 +377,15 @@ - 1 + 1 - 99+ + 99+ - + @@ -403,15 +403,15 @@ - 1 + 1 - 99+ + 99+ - + @@ -424,37 +424,37 @@
- 1 + 1 - 1 + 1 - 1 + 1 - 1 + 1
- 1 + 1 - 1 + 1 - 1 + 1 - 1 + 1
diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts index d167f1e8f64..fe07a4e6baf 100644 --- a/core/src/themes/ios/default.tokens.ts +++ b/core/src/themes/ios/default.tokens.ts @@ -108,7 +108,7 @@ export const defaultTheme: DefaultTheme = { display: 'inline-block', font: { - // size: dynamicFont(13), // TODO: uncomment this when dynamic font is implemented + // size: dynamicFontMin(1, 13), // TODO: uncomment this when dynamic font is implemented size: `13px`, // TODO: remove this when dynamic font is implemented weight: 'var(--ion-font-weight-bold)', }, diff --git a/core/src/themes/md/default.tokens.ts b/core/src/themes/md/default.tokens.ts index fca28e3600c..195001d60ae 100644 --- a/core/src/themes/md/default.tokens.ts +++ b/core/src/themes/md/default.tokens.ts @@ -31,7 +31,7 @@ export const defaultTheme: DefaultTheme = { components: { IonBadge: { hue: 'bold', - shape: 'crisp', + shape: 'soft', size: 'small', }, @@ -153,15 +153,9 @@ export const defaultTheme: DefaultTheme = { // Shapes shape: { - crisp: { - border: { - radius: 'var(--ion-radii-sm)', - }, - }, - soft: { border: { - radius: 'var(--ion-radii-md)', + radius: 'var(--ion-radii-sm)', }, }, From 9c691b4ac5b7eb70ef06ad11d4cc9b0dbab72821 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 31 Mar 2026 11:14:41 -0700 Subject: [PATCH 4/5] feat(avatar, badge, tab-button): a lot of avatar updates --- core/src/components.d.ts | 8 +- core/src/components/avatar/avatar.common.scss | 303 ++++++++++- core/src/components/avatar/avatar.ionic.scss | 73 --- core/src/components/badge/badge.ionic.scss | 14 +- core/src/components/badge/badge.scss | 58 +- core/src/components/badge/badge.tsx | 5 +- .../src/components/badge/test/hint/index.html | 178 +++--- .../tab-button/tab-button.ionic.scss | 9 - core/src/themes/ionic/default.tokens.ts | 505 +++++++++++++++++- core/src/themes/ios/default.tokens.ts | 43 +- core/src/themes/md/default.tokens.ts | 90 ++-- core/src/themes/themes.interfaces.ts | 3 + 12 files changed, 1013 insertions(+), 276 deletions(-) diff --git a/core/src/components.d.ts b/core/src/components.d.ts index b3a6b2f5246..34135a5a2d1 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -477,9 +477,9 @@ export namespace Components { */ "mode"?: "ios" | "md"; /** - * Set to `"soft"` for a badge with slightly rounded corners, `"round"` for a badge with fully rounded corners, or `"rectangular"` for a badge without rounded corners. Defaults to `"soft"` if both the shape property and theme config are unset. + * Set to `"crisp"` for a badge with even slightly rounded corners, `"soft"` for a badge with slightly rounded corners, `"round"` for a badge with fully rounded corners, or `"rectangular"` for a badge without rounded corners. Defaults to `"soft"` if both the shape property and theme config are unset. */ - "shape"?: 'soft' | 'round' | 'rectangular'; + "shape"?: 'crisp' | 'soft' | 'round' | 'rectangular'; /** * Set to `"small"` for a smaller size. Set to `"medium"` for a medium size. Set to `"large"` for a larger size. Defaults to `"small"` if both the size property and theme config are unset. */ @@ -6408,9 +6408,9 @@ declare namespace LocalJSX { */ "mode"?: "ios" | "md"; /** - * Set to `"soft"` for a badge with slightly rounded corners, `"round"` for a badge with fully rounded corners, or `"rectangular"` for a badge without rounded corners. Defaults to `"soft"` if both the shape property and theme config are unset. + * Set to `"crisp"` for a badge with even slightly rounded corners, `"soft"` for a badge with slightly rounded corners, `"round"` for a badge with fully rounded corners, or `"rectangular"` for a badge without rounded corners. Defaults to `"soft"` if both the shape property and theme config are unset. */ - "shape"?: 'soft' | 'round' | 'rectangular'; + "shape"?: 'crisp' | 'soft' | 'round' | 'rectangular'; /** * Set to `"small"` for a smaller size. Set to `"medium"` for a medium size. Set to `"large"` for a larger size. Defaults to `"small"` if both the size property and theme config are unset. */ diff --git a/core/src/components/avatar/avatar.common.scss b/core/src/components/avatar/avatar.common.scss index e24c047ad72..3ed07d87612 100644 --- a/core/src/components/avatar/avatar.common.scss +++ b/core/src/components/avatar/avatar.common.scss @@ -1,4 +1,4 @@ -@import "../../themes/mixins.scss"; +@use "../../themes/mixins" as mixins; // Avatar // -------------------------------------------------- @@ -7,7 +7,7 @@ /** * @prop --border-radius: Border radius of the avatar and inner image */ - @include border-radius(var(--border-radius)); + @include mixins.border-radius(var(--border-radius)); display: block; @@ -16,7 +16,7 @@ ::slotted(ion-img), ::slotted(img) { - @include border-radius(var(--border-radius)); + @include mixins.border-radius(var(--border-radius)); width: 100%; height: 100%; @@ -25,3 +25,300 @@ overflow: hidden; } + +// Avatar Slotted Elements +// --------------------------------------------- + +// Badge Indicator inside Avatar xxsmall +:host(.avatar-xxsmall) ::slotted(ion-badge.badge-vertical-top:empty) { + @include mixins.transform( + translate( + var(--ion-avatar-size-xxsmall-badge-indicator-empty-top-translate-x), + var(--ion-avatar-size-xxsmall-badge-indicator-empty-top-translate-y) + ) + ); +} + +:host(.avatar-xxsmall) ::slotted(ion-badge.badge-vertical-bottom:empty) { + @include mixins.transform( + translate( + var(--ion-avatar-size-xxsmall-badge-indicator-empty-bottom-translate-x), + var(--ion-avatar-size-xxsmall-badge-indicator-empty-bottom-translate-y) + ) + ); +} + +:host(.avatar-xxsmall) ::slotted(ion-badge.badge-vertical-top:not(:empty)) { + @include mixins.position( + var(--ion-avatar-size-xxsmall-badge-indicator-default-top-position-top), + var(--ion-avatar-size-xxsmall-badge-indicator-default-top-position-end), + var(--ion-avatar-size-xxsmall-badge-indicator-default-top-position-bottom), + var(--ion-avatar-size-xxsmall-badge-indicator-default-top-position-start) + ); + @include mixins.transform( + translate( + var(--ion-avatar-size-xxsmall-badge-indicator-default-top-translate-x), + var(--ion-avatar-size-xxsmall-badge-indicator-default-top-translate-y) + ) + ); +} + +:host(.avatar-xxsmall) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) { + @include mixins.position( + var(--ion-avatar-size-xxsmall-badge-indicator-default-bottom-position-top), + var(--ion-avatar-size-xxsmall-badge-indicator-default-bottom-position-end), + var(--ion-avatar-size-xxsmall-badge-indicator-default-bottom-position-bottom), + var(--ion-avatar-size-xxsmall-badge-indicator-default-bottom-position-start) + ); + @include mixins.transform( + translate( + var(--ion-avatar-size-xxsmall-badge-indicator-default-bottom-translate-x), + var(--ion-avatar-size-xxsmall-badge-indicator-default-bottom-translate-y) + ) + ); +} + +// Badge Indicator inside Avatar xsmall +:host(.avatar-xsmall) ::slotted(ion-badge.badge-vertical-top:empty) { + @include mixins.transform( + translate( + var(--ion-avatar-size-xsmall-badge-indicator-empty-top-translate-x), + var(--ion-avatar-size-xsmall-badge-indicator-empty-top-translate-y) + ) + ); +} + +:host(.avatar-xsmall) ::slotted(ion-badge.badge-vertical-bottom:empty) { + @include mixins.transform( + translate( + var(--ion-avatar-size-xsmall-badge-indicator-empty-bottom-translate-x), + var(--ion-avatar-size-xsmall-badge-indicator-empty-bottom-translate-y) + ) + ); +} + +:host(.avatar-xsmall) ::slotted(ion-badge.badge-vertical-top:not(:empty)) { + @include mixins.position( + var(--ion-avatar-size-xsmall-badge-indicator-default-top-position-top), + var(--ion-avatar-size-xsmall-badge-indicator-default-top-position-end), + var(--ion-avatar-size-xsmall-badge-indicator-default-top-position-bottom), + var(--ion-avatar-size-xsmall-badge-indicator-default-top-position-start) + ); + @include mixins.transform( + translate( + var(--ion-avatar-size-xsmall-badge-indicator-default-top-translate-x), + var(--ion-avatar-size-xsmall-badge-indicator-default-top-translate-y) + ) + ); +} + +:host(.avatar-xsmall) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) { + @include mixins.position( + var(--ion-avatar-size-xsmall-badge-indicator-default-bottom-position-top), + var(--ion-avatar-size-xsmall-badge-indicator-default-bottom-position-end), + var(--ion-avatar-size-xsmall-badge-indicator-default-bottom-position-bottom), + var(--ion-avatar-size-xsmall-badge-indicator-default-bottom-position-start) + ); + @include mixins.transform( + translate( + var(--ion-avatar-size-xsmall-badge-indicator-default-bottom-translate-x), + var(--ion-avatar-size-xsmall-badge-indicator-default-bottom-translate-y) + ) + ); +} + +// Badge Indicator inside Avatar small +:host(.avatar-small) ::slotted(ion-badge.badge-vertical-top:empty) { + @include mixins.transform( + translate( + var(--ion-avatar-size-small-badge-indicator-empty-top-translate-x), + var(--ion-avatar-size-small-badge-indicator-empty-top-translate-y) + ) + ); +} + +:host(.avatar-small) ::slotted(ion-badge.badge-vertical-bottom:empty) { + @include mixins.transform( + translate( + var(--ion-avatar-size-small-badge-indicator-empty-bottom-translate-x), + var(--ion-avatar-size-small-badge-indicator-empty-bottom-translate-y) + ) + ); +} + +:host(.avatar-small) ::slotted(ion-badge.badge-vertical-top:not(:empty)) { + @include mixins.position( + var(--ion-avatar-size-small-badge-indicator-default-top-position-top), + var(--ion-avatar-size-small-badge-indicator-default-top-position-end), + var(--ion-avatar-size-small-badge-indicator-default-top-position-bottom), + var(--ion-avatar-size-small-badge-indicator-default-top-position-start) + ); + @include mixins.transform( + translate( + var(--ion-avatar-size-small-badge-indicator-default-top-translate-x), + var(--ion-avatar-size-small-badge-indicator-default-top-translate-y) + ) + ); +} + +:host(.avatar-small) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) { + @include mixins.position( + var(--ion-avatar-size-small-badge-indicator-default-bottom-position-top), + var(--ion-avatar-size-small-badge-indicator-default-bottom-position-end), + var(--ion-avatar-size-small-badge-indicator-default-bottom-position-bottom), + var(--ion-avatar-size-small-badge-indicator-default-bottom-position-start) + ); + @include mixins.transform( + translate( + var(--ion-avatar-size-small-badge-indicator-default-bottom-translate-x), + var(--ion-avatar-size-small-badge-indicator-default-bottom-translate-y) + ) + ); +} + +// Badge Indicator inside Avatar medium +:host(.avatar-medium) ::slotted(ion-badge.badge-vertical-top:empty) { + @include mixins.transform( + translate( + var(--ion-avatar-size-medium-badge-indicator-empty-top-translate-x), + var(--ion-avatar-size-medium-badge-indicator-empty-top-translate-y) + ) + ); +} + +:host(.avatar-medium) ::slotted(ion-badge.badge-vertical-bottom:empty) { + @include mixins.transform( + translate( + var(--ion-avatar-size-medium-badge-indicator-empty-bottom-translate-x), + var(--ion-avatar-size-medium-badge-indicator-empty-bottom-translate-y) + ) + ); +} + +:host(.avatar-medium) ::slotted(ion-badge.badge-vertical-top:not(:empty)) { + @include mixins.position( + var(--ion-avatar-size-medium-badge-indicator-default-top-position-top), + var(--ion-avatar-size-medium-badge-indicator-default-top-position-end), + var(--ion-avatar-size-medium-badge-indicator-default-top-position-bottom), + var(--ion-avatar-size-medium-badge-indicator-default-top-position-start) + ); + @include mixins.transform( + translate( + var(--ion-avatar-size-medium-badge-indicator-default-top-translate-x), + var(--ion-avatar-size-medium-badge-indicator-default-top-translate-y) + ) + ); +} + +:host(.avatar-medium) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) { + @include mixins.position( + var(--ion-avatar-size-medium-badge-indicator-default-bottom-position-top), + var(--ion-avatar-size-medium-badge-indicator-default-bottom-position-end), + var(--ion-avatar-size-medium-badge-indicator-default-bottom-position-bottom), + var(--ion-avatar-size-medium-badge-indicator-default-bottom-position-start) + ); + @include mixins.transform( + translate( + var(--ion-avatar-size-medium-badge-indicator-default-bottom-translate-x), + var(--ion-avatar-size-medium-badge-indicator-default-bottom-translate-y) + ) + ); +} + +// Badge Indicator inside Avatar large +:host(.avatar-large) ::slotted(ion-badge.badge-vertical-top:empty) { + @include mixins.transform( + translate( + var(--ion-avatar-size-large-badge-indicator-empty-top-translate-x), + var(--ion-avatar-size-large-badge-indicator-empty-top-translate-y) + ) + ); +} + +:host(.avatar-large) ::slotted(ion-badge.badge-vertical-bottom:empty) { + @include mixins.transform( + translate( + var(--ion-avatar-size-large-badge-indicator-empty-bottom-translate-x), + var(--ion-avatar-size-large-badge-indicator-empty-bottom-translate-y) + ) + ); +} + +:host(.avatar-large) ::slotted(ion-badge.badge-vertical-top:not(:empty)) { + @include mixins.position( + var(--ion-avatar-size-large-badge-indicator-default-top-position-top), + var(--ion-avatar-size-large-badge-indicator-default-top-position-end), + var(--ion-avatar-size-large-badge-indicator-default-top-position-bottom), + var(--ion-avatar-size-large-badge-indicator-default-top-position-start) + ); + @include mixins.transform( + translate( + var(--ion-avatar-size-large-badge-indicator-default-top-translate-x), + var(--ion-avatar-size-large-badge-indicator-default-top-translate-y) + ) + ); +} + +:host(.avatar-large) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) { + @include mixins.position( + var(--ion-avatar-size-large-badge-indicator-default-bottom-position-top), + var(--ion-avatar-size-large-badge-indicator-default-bottom-position-end), + var(--ion-avatar-size-large-badge-indicator-default-bottom-position-bottom), + var(--ion-avatar-size-large-badge-indicator-default-bottom-position-start) + ); + @include mixins.transform( + translate( + var(--ion-avatar-size-large-badge-indicator-default-bottom-translate-x), + var(--ion-avatar-size-large-badge-indicator-default-bottom-translate-y) + ) + ); +} + +// Badge Indicator inside Avatar xlarge +:host(.avatar-xlarge) ::slotted(ion-badge.badge-vertical-top:empty) { + @include mixins.transform( + translate( + var(--ion-avatar-size-xlarge-badge-indicator-empty-top-translate-x), + var(--ion-avatar-size-xlarge-badge-indicator-empty-top-translate-y) + ) + ); +} + +:host(.avatar-xlarge) ::slotted(ion-badge.badge-vertical-bottom:empty) { + @include mixins.transform( + translate( + var(--ion-avatar-size-xlarge-badge-indicator-empty-bottom-translate-x), + var(--ion-avatar-size-xlarge-badge-indicator-empty-bottom-translate-y) + ) + ); +} + +:host(.avatar-xlarge) ::slotted(ion-badge.badge-vertical-top:not(:empty)) { + @include mixins.position( + var(--ion-avatar-size-xlarge-badge-indicator-default-top-position-top), + var(--ion-avatar-size-xlarge-badge-indicator-default-top-position-end), + var(--ion-avatar-size-xlarge-badge-indicator-default-top-position-bottom), + var(--ion-avatar-size-xlarge-badge-indicator-default-top-position-start) + ); + @include mixins.transform( + translate( + var(--ion-avatar-size-xlarge-badge-indicator-default-top-translate-x), + var(--ion-avatar-size-xlarge-badge-indicator-default-top-translate-y) + ) + ); +} + +:host(.avatar-xlarge) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) { + @include mixins.position( + var(--ion-avatar-size-xlarge-badge-indicator-default-bottom-position-top), + var(--ion-avatar-size-xlarge-badge-indicator-default-bottom-position-end), + var(--ion-avatar-size-xlarge-badge-indicator-default-bottom-position-bottom), + var(--ion-avatar-size-xlarge-badge-indicator-default-bottom-position-start) + ); + @include mixins.transform( + translate( + var(--ion-avatar-size-xlarge-badge-indicator-default-bottom-translate-x), + var(--ion-avatar-size-xlarge-badge-indicator-default-bottom-translate-y) + ) + ); +} diff --git a/core/src/components/avatar/avatar.ionic.scss b/core/src/components/avatar/avatar.ionic.scss index 11ffcdc881a..d31e6c9c5cb 100644 --- a/core/src/components/avatar/avatar.ionic.scss +++ b/core/src/components/avatar/avatar.ionic.scss @@ -169,79 +169,6 @@ height: globals.$ion-scale-800; } -// Avatar Badge Empty (hint) -// -------------------------------------------------- - -:host ::slotted(ion-badge.badge-vertical-top:empty) { - @include globals.transform(translate(globals.$ion-scale-050, calc(globals.$ion-scale-050 * -1))); -} - -:host(.avatar-xxsmall) ::slotted(ion-badge.badge-vertical-top:empty) { - @include globals.transform(translate(globals.$ion-scale-100, calc(globals.$ion-scale-100 * -1))); -} - -:host ::slotted(ion-badge.badge-vertical-bottom:empty) { - @include globals.transform(translate(0, globals.$ion-scale-100)); -} - -:host(.avatar-xxsmall) ::slotted(ion-badge.badge-vertical-bottom:empty) { - @include globals.transform(translate(globals.$ion-scale-100, globals.$ion-scale-100)); -} - -// Avatar Badge Bottom (hint) -// -------------------------------------------------- - -:host ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) { - @include globals.transform(translate(50%, 50%)); -} - -:host(.avatar-xxsmall) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) { - @include globals.position(null, globals.$ion-scale-100, globals.$ion-scale-100, null); - @include globals.transform(translate(100%, 100%)); -} - -:host(.avatar-xsmall) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) { - @include globals.position(null, calc(globals.$ion-scale-050 * -1), calc(globals.$ion-scale-050 * -1), null); -} - -:host(.avatar-small) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)), -:host(.avatar-medium) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)), -:host(.avatar-large) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) { - @include globals.position(null, globals.$ion-scale-050, globals.$ion-scale-050, null); -} - -:host(.avatar-xlarge) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) { - @include globals.position(null, globals.$ion-scale-150, globals.$ion-scale-150, null); -} - -// Avatar Badge Top (hint) -// -------------------------------------------------- - -:host ::slotted(ion-badge.badge-vertical-top:not(:empty)) { - @include globals.transform(translate(50%, -50%)); -} - -:host(.avatar-xxsmall) ::slotted(ion-badge.badge-vertical-top:not(:empty)) { - @include globals.position(globals.$ion-scale-050, 0, null, null); -} - -:host(.avatar-xsmall) ::slotted(ion-badge.badge-vertical-top:not(:empty)) { - @include globals.position(globals.$ion-scale-100, calc(globals.$ion-scale-050 * -1), null, null); -} - -:host(.avatar-small) ::slotted(ion-badge.badge-vertical-top:not(:empty)), -:host(.avatar-medium) ::slotted(ion-badge.badge-vertical-top:not(:empty)) { - @include globals.position(globals.$ion-scale-150, 0, null, null); -} - -:host(.avatar-large) ::slotted(ion-badge.badge-vertical-top:not(:empty)) { - @include globals.position(globals.$ion-scale-150, globals.$ion-scale-050, null, null); -} - -:host(.avatar-xlarge) ::slotted(ion-badge.badge-vertical-top:not(:empty)) { - @include globals.position(globals.$ion-scale-150, globals.$ion-scale-150, null, null); -} - // Avatar Disabled // -------------------------------------------------- :host(.avatar-disabled)::after { diff --git a/core/src/components/badge/badge.ionic.scss b/core/src/components/badge/badge.ionic.scss index b8e36943689..8df5f000de2 100644 --- a/core/src/components/badge/badge.ionic.scss +++ b/core/src/components/badge/badge.ionic.scss @@ -10,7 +10,7 @@ --padding-top: #{globals.$ion-space-0}; --padding-bottom: #{globals.$ion-space-0}; - @include globals.typography(globals.$ion-body-sm-medium); + @include globals.typography(globals.$ion-body-sm-medium); // TODO: Add this display: inline-flex; @@ -82,7 +82,8 @@ } :host(.badge-small) ::slotted(ion-icon) { - width: globals.$ion-scale-300; + @include globals.typography(globals.$ion-body-sm-medium); // TODO: Add this + width: globals.$ion-scale-300; // 12px height: globals.$ion-scale-300; } @@ -162,17 +163,18 @@ // Badge in Button // -------------------------------------------------- +// TODO: add example that this should be using the small size within the ion-button :host(:not(:empty).in-button) { - --padding-start: #{globals.$ion-scale-050}; + --padding-start: #{globals.$ion-scale-050}; // not needed since it is the same as small, give example that it should be using small size --padding-end: #{globals.$ion-scale-050}; - @include globals.typography(globals.$ion-body-action-xs); + @include globals.typography(globals.$ion-body-action-xs); // this is wrong when compared to designs - min-width: globals.$ion-scale-400; + min-width: globals.$ion-scale-400; // not needed since it is the same as small, give example that it should be using small size height: globals.$ion-scale-400; ::slotted(ion-icon) { - width: globals.$ion-scale-300; + width: globals.$ion-scale-300; // not needed since it is the same as small, give example that it should be using small size height: globals.$ion-scale-300; } } diff --git a/core/src/components/badge/badge.scss b/core/src/components/badge/badge.scss index dfe6ecafbd3..dd22a1b2c31 100644 --- a/core/src/components/badge/badge.scss +++ b/core/src/components/badge/badge.scss @@ -16,14 +16,10 @@ */ @include mixins.font-smoothing(); - display: var(--ion-badge-display); + display: inline-flex; - font-size: var(--ion-badge-font-size); - font-weight: var(--ion-badge-font-weight); - - line-height: var(--ion-badge-line-height); - - text-align: center; + align-items: center; + justify-content: center; white-space: nowrap; @@ -90,6 +86,11 @@ min-width: var(--ion-badge-size-small-default-min-width); height: var(--ion-badge-size-small-default-height); + + font-size: var(--ion-badge-size-small-default-font-size); + font-weight: var(--ion-badge-size-small-default-font-weight); + + line-height: var(--ion-badge-size-small-default-line-height); } :host(.badge-small) ::slotted(ion-icon) { @@ -108,6 +109,11 @@ min-width: var(--ion-badge-size-medium-default-min-width); height: var(--ion-badge-size-medium-default-height); + + font-size: var(--ion-badge-size-medium-default-font-size); + font-weight: var(--ion-badge-size-medium-default-font-weight); + + line-height: var(--ion-badge-size-medium-default-line-height); } :host(.badge-medium) ::slotted(ion-icon) { @@ -126,6 +132,11 @@ min-width: var(--ion-badge-size-large-default-min-width); height: var(--ion-badge-size-large-default-height); + + font-size: var(--ion-badge-size-large-default-font-size); + font-weight: var(--ion-badge-size-large-default-font-weight); + + line-height: var(--ion-badge-size-large-default-line-height); } :host(.badge-large) ::slotted(ion-icon) { @@ -179,36 +190,5 @@ // TODO: remove from the tab-button.common // In Tab Button :host([vertical].in-tab-button) { - position: var(--ion-badge-indicator-in-tab-button-default-position); -} - -// Slotted Icon -:host([vertical]) ::slotted(ion-icon) { - @include mixins.position( - var( - --ion-badge-indicator-icon-position-top, - --ion-badge-indicator-icon-position-end, - --ion-badge-indicator-icon-position-bottom, - --ion-badge-indicator-icon-position-start - ) - ); - - position: var(--ion-badge-indicator-icon-position); - - transform: translate(var(--ion-badge-indicator-icon-translate-x), var(--ion-badge-indicator-icon-translate-y)); -} - -// In Button -:host(.in-button:not(:empty)) { - min-width: var(--ion-badge-indicator-in-button-default-min-width); - height: var(--ion-badge-indicator-in-button-default-height); - - font-size: var(--ion-badge-indicator-in-button-default-font-size); - - line-height: var(--ion-badge-indicator-in-button-default-line-height); -} - -:host(.in-button:not(:empty)) ::slotted(ion-icon) { - width: var(--ion-badge-indicator-in-button-default-icon-width, revert-layer); - height: var(--ion-badge-indicator-in-button-default-icon-height, revert-layer); + position: var(--ion-tab-button-badge-indicator-position); } diff --git a/core/src/components/badge/badge.tsx b/core/src/components/badge/badge.tsx index 86a280956fe..28e1c258e98 100644 --- a/core/src/components/badge/badge.tsx +++ b/core/src/components/badge/badge.tsx @@ -34,13 +34,14 @@ export class Badge implements ComponentInterface { @Prop() hue?: 'bold' | 'subtle'; /** - * Set to `"soft"` for a badge with slightly rounded corners, + * Set to `"crisp"` for a badge with even slightly rounded corners, + * `"soft"` for a badge with slightly rounded corners, * `"round"` for a badge with fully rounded corners, * or `"rectangular"` for a badge without rounded corners. * * Defaults to `"soft"` if both the shape property and theme config are unset. */ - @Prop() shape?: 'soft' | 'round' | 'rectangular'; + @Prop() shape?: 'crisp' | 'soft' | 'round' | 'rectangular'; /** * Set to `"small"` for a smaller size. diff --git a/core/src/components/badge/test/hint/index.html b/core/src/components/badge/test/hint/index.html index dfd462aa329..0c1f1fab827 100644 --- a/core/src/components/badge/test/hint/index.html +++ b/core/src/components/badge/test/hint/index.html @@ -17,6 +17,14 @@ ion-tab-bar { width: 100%; } + + .row { + display: flex; + flex-direction: row; + justify-content: space-around; + align-items: center; + margin: 16px; + } @@ -35,15 +43,15 @@ Badge small - + Badge Medium - + Badge Large - + @@ -52,184 +60,184 @@ Inside Avatar -
+
- + - + - + - + - + - +
-
+
- + - + - + - + - + - +
-
+
- 1 + 1 - 1 + 1 - 1 + 1 - 1 + 1 - 1 + 1 - 1 + 1
-
+
- 1 + 1 - 1 + 1 - 1 + 1 - 1 + 1 - 1 + 1 - 1 + 1
-
+
- + - + - + - + - + - +
-
+
- + - + - + - + - + - + @@ -241,18 +249,18 @@ Inside Tab Button - Top Icon -
+
Tab One - 9 + 9 Tab Two - + @@ -260,28 +268,28 @@ Tab Three - 999 + 999 Tab Four - +
-
+
Tab One - 9 + 9 Tab Two - + @@ -289,13 +297,13 @@ Tab Three - 999 + 999 Tab Four - +
@@ -306,18 +314,18 @@ Inside Tab Button - Bottom Icon -
+
Tab One - 9 + 9 Tab Two - + @@ -325,28 +333,28 @@ Tab Three - 999 + 999 Tab Four - +
-
+
Tab One - 9 + 9 Tab Two - + @@ -354,13 +362,13 @@ Tab Three - 999 + 999 Tab Four - +
@@ -370,22 +378,22 @@ Inside Button - Top Placement -
+
- + - 1 + 1 - 99+ + 99+ - + @@ -396,22 +404,22 @@ Inside Button - Bottom Placement -
+
- + - 1 + 1 - 99+ + 99+ - + @@ -421,40 +429,40 @@ Inside Button - Button Size -
+
- 1 + 1 - 1 + 1 - 1 + 1 - 1 + 1
-
+
- 1 + 1 - 1 + 1 - 1 + 1 - 1 + 1
diff --git a/core/src/components/tab-button/tab-button.ionic.scss b/core/src/components/tab-button/tab-button.ionic.scss index d2a77603305..65c7e9b2272 100644 --- a/core/src/components/tab-button/tab-button.ionic.scss +++ b/core/src/components/tab-button/tab-button.ionic.scss @@ -122,15 +122,6 @@ @include globals.position(calc(50% - globals.$ion-scale-100)); } -:host ::slotted(ion-badge[vertical]:not(:empty)) { - @include globals.padding(globals.$ion-scale-100); - - display: flex; - - align-items: center; - justify-content: center; -} - :host(.tab-layout-icon-bottom) ::slotted(ion-badge.badge-vertical-top) { @include globals.position(calc(50% - globals.$ion-scale-100)); } diff --git a/core/src/themes/ionic/default.tokens.ts b/core/src/themes/ionic/default.tokens.ts index 5c9835be6d5..4f2b5272a85 100644 --- a/core/src/themes/ionic/default.tokens.ts +++ b/core/src/themes/ionic/default.tokens.ts @@ -1,4 +1,4 @@ -import { currentColor, mix, dynamicFont } from '../../utils/theme'; +import { currentColor, mix, dynamicFont, ionColor } from '../../utils/theme'; import { defaultTheme as baseDefaultTheme } from '../base/default.tokens'; import { colors as baseColors } from '../base/shared.tokens'; import type { DefaultTheme } from '../themes.interfaces'; @@ -100,6 +100,501 @@ export const defaultTheme: DefaultTheme = { }, components: { + IonAvatar: { + size: { + xxsmall: { + badge: { + indicator: { + default: { + top: { + translate: { + x: '50%', + y: '-50%', + }, + + position: { + top: 'var(--ion-scaling-50)', + end: 'var(--ion-scaling-0)', + }, + }, + + bottom: { + translate: { + x: '100%', + y: '100%', + }, + + position: { + end: 'var(--ion-scaling-100)', + bottom: 'var(--ion-scaling-100)', + }, + }, + }, + + empty: { + top: { + translate: { + x: 'var(--ion-scaling-100)', + y: 'calc(var(--ion-scaling-100) * -1)', + }, + }, + + bottom: { + translate: { + x: 'var(--ion-scaling-100)', + y: 'var(--ion-scaling-100)', + }, + }, + }, + }, + }, + }, + + xsmall: { + badge: { + indicator: { + default: { + top: { + translate: { + x: '50%', + y: '-50%', + }, + + position: { + top: 'var(--ion-scaling-100)', + end: 'var(--ion-scaling-50)', + }, + }, + + bottom: { + translate: { + x: '50%', + y: '50%', + }, + + position: { + end: 'calc(var(--ion-scaling-50) * -1)', + bottom: 'calc(var(--ion-scaling-50) * -1)', + }, + }, + }, + + empty: { + top: { + translate: { + x: 'var(--ion-scaling-50)', + y: 'calc(var(--ion-scaling-50) * -1)', + }, + }, + + bottom: { + translate: { + x: 'var(--ion-scaling-0)', + y: 'var(--ion-scaling-100)', + }, + }, + }, + }, + }, + }, + + small: { + badge: { + indicator: { + default: { + top: { + translate: { + x: '50%', + y: '-50%', + }, + + position: { + top: 'var(--ion-scaling-150)', + end: 'var(--ion-scaling-0)', + }, + }, + + bottom: { + translate: { + x: '50%', + y: '50%', + }, + + position: { + end: 'var(--ion-scaling-50)', + bottom: 'var(--ion-scaling-50)', + }, + }, + }, + + empty: { + top: { + translate: { + x: 'var(--ion-scaling-50)', + y: 'calc(var(--ion-scaling-50) * -1)', + }, + }, + + bottom: { + translate: { + x: 'var(--ion-scaling-0)', + y: 'var(--ion-scaling-100)', + }, + }, + }, + }, + }, + }, + + medium: { + badge: { + indicator: { + default: { + top: { + translate: { + x: '50%', + y: '-50%', + }, + + position: { + top: 'var(--ion-scaling-150)', + end: 'var(--ion-scaling-0)', + }, + }, + + bottom: { + translate: { + x: '50%', + y: '50%', + }, + + position: { + end: 'var(--ion-scaling-50)', + bottom: 'var(--ion-scaling-50)', + }, + }, + }, + + empty: { + top: { + translate: { + x: 'var(--ion-scaling-50)', + y: 'calc(var(--ion-scaling-50) * -1)', + }, + }, + + bottom: { + translate: { + x: 'var(--ion-scaling-0)', + y: 'var(--ion-scaling-100)', + }, + }, + }, + }, + }, + }, + + large: { + badge: { + indicator: { + default: { + top: { + translate: { + x: '50%', + y: '-50%', + }, + + position: { + top: 'var(--ion-scaling-150)', + end: 'var(--ion-scaling-50)', + }, + }, + + bottom: { + translate: { + x: '50%', + y: '50%', + }, + + position: { + end: 'var(--ion-scaling-50)', + bottom: 'var(--ion-scaling-50)', + }, + }, + }, + + empty: { + top: { + translate: { + x: 'var(--ion-scaling-50)', + y: 'calc(var(--ion-scaling-50) * -1)', + }, + }, + + bottom: { + translate: { + x: 'var(--ion-scaling-0)', + y: 'var(--ion-scaling-100)', + }, + }, + }, + }, + }, + }, + + xlarge: { + badge: { + indicator: { + default: { + top: { + translate: { + x: '50%', + y: '-50%', + }, + + position: { + top: 'var(--ion-scaling-150)', + end: 'var(--ion-scaling-150)', + }, + }, + + bottom: { + translate: { + x: '50%', + y: '50%', + }, + + position: { + end: 'var(--ion-scaling-150)', + bottom: 'var(--ion-scaling-150)', + }, + }, + }, + + empty: { + top: { + translate: { + x: 'var(--ion-scaling-50)', + y: 'calc(var(--ion-scaling-50) * -1)', + }, + }, + + bottom: { + translate: { + x: 'var(--ion-scaling-0)', + y: 'var(--ion-scaling-100)', + }, + }, + }, + }, + }, + }, + }, + }, + + IonBadge: { + // Hues + hue: { + bold: { + default: { + background: ionColor('primary', 'base'), + color: ionColor('primary', 'contrast'), + }, + + semantic: { + default: { + background: currentColor('base'), + color: currentColor('contrast'), + }, + }, + }, + + subtle: { + default: { + background: ionColor('primary', 'base', { subtle: true }), + color: ionColor('primary', 'contrast', { subtle: true }), + }, + + semantic: { + default: { + background: currentColor('base', { subtle: true }), + color: currentColor('contrast', { subtle: true }), + }, + }, + }, + }, + + // Shapes + shape: { + crisp: { + border: { + radius: 'var(--ion-radii-sm)', + }, + }, + + soft: { + border: { + radius: 'var(--ion-radii-md)', + }, + }, + + round: { + border: { + radius: 'var(--ion-radii-xxxxl)', + }, + }, + + rectangular: { + border: { + radius: 'var(--ion-radii-xxxxs)', + }, + }, + }, + + // Sizes + size: { + small: { + default: { + height: 'var(--ion-scaling-xxxs)', + letterSpacing: '0%', + + min: { + width: 'var(--ion-scaling-xxxs)', + }, + + padding: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-xxxs)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-xxxs)', + }, + + font: { + size: 'var(--ion-font-size-xs)', + weight: 'var(--ion-font-weight-medium)', + }, + + line: { + height: 'var(--ion-scaling-xxs)', + }, + + icon: { + width: 'var(--ion-scaling-xxxxs)', + height: 'var(--ion-scaling-xxxxs)', + }, + }, + + empty: { + height: 'var(--ion-scaling-200)', + + min: { + width: 'var(--ion-scaling-200)', + }, + }, + }, + + medium: { + default: { + height: 'var(--ion-scaling-xs)', + letterSpacing: '0%', + + min: { + width: 'var(--ion-scaling-xs)', + }, + + padding: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-xxs)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-xxs)', + }, + + font: { + size: 'var(--ion-font-size-sm)', + weight: 'var(--ion-font-weight-medium)', + }, + + line: { + height: 'var(--ion-scaling-xs)', + }, + + icon: { + width: 'var(--ion-scaling-xxxs)', + height: 'var(--ion-scaling-xxxs)', + }, + }, + + empty: { + height: 'var(--ion-scaling-xxxxs)', + + min: { + width: 'var(--ion-scaling-xxxxs)', + }, + }, + }, + + large: { + default: { + height: 'var(--ion-scaling-xs)', + letterSpacing: '0%', + + min: { + width: 'var(--ion-scaling-xs)', + }, + + padding: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-xxs)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-xxs)', + }, + + font: { + size: 'var(--ion-font-size-sm)', + weight: 'var(--ion-font-weight-medium)', + }, + + line: { + height: 'var(--ion-scaling-xs)', + }, + + icon: { + width: 'var(--ion-scaling-xxxs)', + height: 'var(--ion-scaling-xxxs)', + }, + }, + + empty: { + height: 'var(--ion-scaling-xxxs)', + + min: { + width: 'var(--ion-scaling-xxxs)', + }, + }, + }, + }, + + indicator: { + height: 'var(--ion-scaling-xxxs)', + + min: { + width: 'var(--ion-scaling-xxxs)', + }, + + padding: { + top: 'var(--ion-spacing-0)', + end: 'var(--ion-spacing-xxxs)', + bottom: 'var(--ion-spacing-0)', + start: 'var(--ion-spacing-xxxs)', + }, + + icon: { + width: 'var(--ion-scaling-xxxxs)', + height: 'var(--ion-scaling-xxxxs)', + }, + }, + }, + IonChip: { margin: { top: 'var(--ion-spacing-0)', @@ -542,5 +1037,13 @@ export const defaultTheme: DefaultTheme = { }, }, }, + + IonTabButton: { + badge: { + indicator: { + position: 'relative', + }, + }, + }, }, }; diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts index b9fec3de73d..ded3d5f2c4f 100644 --- a/core/src/themes/ios/default.tokens.ts +++ b/core/src/themes/ios/default.tokens.ts @@ -102,18 +102,6 @@ export const defaultTheme: DefaultTheme = { components: { IonBadge: { - display: 'inline-block', - - font: { - // size: dynamicFontMin(1, 13), // TODO: uncomment this when dynamic font is implemented - size: `13px`, // TODO: remove this when dynamic font is implemented - weight: 'var(--ion-font-weight-bold)', - }, - - line: { - height: '1', - }, - // Hues hue: { bold: { @@ -186,6 +174,15 @@ export const defaultTheme: DefaultTheme = { bottom: 'var(--ion-spacing-xxs)', start: 'var(--ion-spacing-xxs)', }, + + font: { + size: dynamicFont(global.root, 13), + weight: 'var(--ion-font-weight-bold)', + }, + + line: { + height: '1', + }, }, empty: { @@ -209,6 +206,15 @@ export const defaultTheme: DefaultTheme = { bottom: 'var(--ion-spacing-xs)', start: 'var(--ion-spacing-xs)', }, + + font: { + size: dynamicFont(global.root, 13), + weight: 'var(--ion-font-weight-bold)', + }, + + line: { + height: '1', + }, }, empty: { @@ -232,6 +238,15 @@ export const defaultTheme: DefaultTheme = { bottom: 'var(--ion-spacing-sm)', start: 'var(--ion-spacing-sm)', }, + + font: { + size: dynamicFont(global.root, 13), + weight: 'var(--ion-font-weight-bold)', + }, + + line: { + height: '1', + }, }, empty: { @@ -277,10 +292,6 @@ export const defaultTheme: DefaultTheme = { }, }, }, - - icon: { - position: {}, - }, }, }, diff --git a/core/src/themes/md/default.tokens.ts b/core/src/themes/md/default.tokens.ts index aef5b39f7c6..e47991fb457 100644 --- a/core/src/themes/md/default.tokens.ts +++ b/core/src/themes/md/default.tokens.ts @@ -105,18 +105,11 @@ export const defaultTheme: DefaultTheme = { components: { IonBadge: { - display: 'inline-block', - font: { - // size: dynamicFont(13), // TODO: uncomment this when dynamic font is implemented - size: `13px`, // TODO: remove this when dynamic font is implemented + size: dynamicFont(global.root, 13), weight: 'var(--ion-font-weight-bold)', }, - line: { - height: '1', - }, - // Hues hue: { bold: { @@ -183,6 +176,15 @@ export const defaultTheme: DefaultTheme = { bottom: 'var(--ion-spacing-xxs)', start: 'var(--ion-spacing-xxs)', }, + + font: { + size: dynamicFont(global.root, 13), + weight: 'var(--ion-font-weight-bold)', + }, + + line: { + height: '1', + }, }, empty: { @@ -206,6 +208,15 @@ export const defaultTheme: DefaultTheme = { bottom: 'var(--ion-spacing-xs)', start: 'var(--ion-spacing-xs)', }, + + font: { + size: dynamicFont(global.root, 13), + weight: 'var(--ion-font-weight-bold)', + }, + + line: { + height: '1', + }, }, empty: { @@ -229,6 +240,15 @@ export const defaultTheme: DefaultTheme = { bottom: 'var(--ion-spacing-sm)', start: 'var(--ion-spacing-sm)', }, + + font: { + size: dynamicFont(global.root, 13), + weight: 'var(--ion-font-weight-bold)', + }, + + line: { + height: '1', + }, }, empty: { @@ -240,43 +260,29 @@ export const defaultTheme: DefaultTheme = { }, }, }, + }, - indicator: { - in: { - button: { - default: { - height: 'var(--ion-scaling-xxxs)', - - min: { - width: 'var(--ion-scaling-xxxs)', - }, - - line: { - height: 'var(--ion-scaling-xxs)', - }, + IonButton: { + badge: { + indicator: { + height: 'var(--ion-scaling-xxxs)', - font: { - size: `${(12 / global.root).toFixed(2)}rem`, - }, + min: { + width: 'var(--ion-scaling-xxxs)', + }, - icon: { - width: 'var(--ion-scaling-xxxxs)', - height: 'var(--ion-scaling-xxxxs)', - }, - }, + line: { + height: 'var(--ion-scaling-xxs)', }, - tab: { - button: { - default: { - position: 'absolute', - }, - }, + font: { + size: `${(12 / global.root).toFixed(2)}rem`, }, - }, - icon: { - position: {}, + icon: { + width: 'var(--ion-scaling-xxxxs)', + height: 'var(--ion-scaling-xxxxs)', + }, }, }, }, @@ -850,5 +856,13 @@ export const defaultTheme: DefaultTheme = { }, }, }, + + IonTabButton: { + badge: { + indicator: { + position: 'absolute', + }, + }, + }, }, }; diff --git a/core/src/themes/themes.interfaces.ts b/core/src/themes/themes.interfaces.ts index 24a606f2e21..fec9edb0d88 100644 --- a/core/src/themes/themes.interfaces.ts +++ b/core/src/themes/themes.interfaces.ts @@ -281,10 +281,13 @@ export type DefaultTheme = BaseTheme & { }; type Components = { + IonAvatar?: any; IonBadge?: any; + IonButton?: any; IonChip?: IonChipRecipe; IonItemDivider?: IonItemDividerRecipe; IonSpinner?: IonSpinnerRecipe; + IonTabButton?: any; IonCard?: any; IonItem?: any; From b40e7332b7486366e6285b138594c9850872e247 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 31 Mar 2026 16:22:33 -0700 Subject: [PATCH 5/5] feat(avatar, badge): implement all combos --- core/src/components/avatar/avatar.badge.scss | 139 ++++++++ core/src/components/avatar/avatar.common.scss | 298 +---------------- core/src/components/avatar/avatar.tsx | 6 - .../components/avatar/test/hint/index.html | 97 ++++++ core/src/components/button/button.common.scss | 14 +- core/src/themes/ionic/default.tokens.ts | 302 +----------------- 6 files changed, 251 insertions(+), 605 deletions(-) create mode 100644 core/src/components/avatar/avatar.badge.scss create mode 100644 core/src/components/avatar/test/hint/index.html diff --git a/core/src/components/avatar/avatar.badge.scss b/core/src/components/avatar/avatar.badge.scss new file mode 100644 index 00000000000..42b6cb70a37 --- /dev/null +++ b/core/src/components/avatar/avatar.badge.scss @@ -0,0 +1,139 @@ +@use "../../themes/mixins" as mixins; + +// Avatar Slotted Badge +// --------------------------------------------- + +/** + * Positions an empty badge (status dot) at the correct offset, translating the badge itself to account for its dimensions since it has no content to center. The badge is translated on a 45° angle from the avatar edge. + * 0.1464 = (1 - cos(45°)) / 2 + * + * offset-x = avatarWidth * 0.1464 - badgeWidth / 2 + * offset-y = avatarHeight * 0.1464 - badgeHeight / 2 + * + * @param $avatar-width The width of the avatar + * @param $avatar-height The height of the avatar + * @param $badge-width The width of the badge + * @param $badge-height The height of the badge + * @param $position The position of the badge (top or bottom) + */ +@mixin badge-empty-translate($avatar-width, $avatar-height, $badge-width, $badge-height, $position) { + @include mixins.position(null, 0, null, null); + + @if $position == top { + @include mixins.transform( + translate( + calc((#{$avatar-width} * 0.1464 - #{$badge-width} / 2) * -1), + calc(#{$avatar-height} * 0.1464 - #{$badge-height} / 2) + ) + ); + } @else { + @include mixins.transform( + translate( + calc((#{$avatar-width} * 0.1464 - #{$badge-width} / 2) * -1), + calc((#{$avatar-height} * 0.1464 - #{$badge-height} / 2) * -1) + ) + ); + } +} + +/** + * Positions a non-empty badge at the correct offset from the avatar edge, then translates 50% to center the badge's content. The badge is translated on a 45° angle from the avatar edge. +* 0.1464 = (1 - cos(45°)) / 2 + * + * anchor-x = avatarWidth * 0.1464 + * anchor-y = avatarHeight * 0.1464 + * + * @param $avatar-width The width of the avatar + * @param $avatar-height The height of the avatar + * @param $position The position of the badge (top or bottom) + */ +@mixin badge-default-translate($avatar-width, $avatar-height, $position) { + @if $position == top { + @include mixins.transform(translate(50%, -50%)); + + top: calc(#{$avatar-height} * 0.1464); + bottom: auto; + } @else { + @include mixins.transform(translate(50%, 50%)); + + top: auto; + bottom: calc(#{$avatar-height} * 0.1464); + } + + @include mixins.position(null, calc(#{$avatar-width} * 0.1464), null, null); +} + +// Maps +$avatar-sizes: ( + xxsmall: ( + width: var(--ion-avatar-size-xxsmall-width, 16px), + height: var(--ion-avatar-size-xxsmall-height, 16px), + ), + xsmall: ( + width: var(--ion-avatar-size-xsmall-width, 24px), + height: var(--ion-avatar-size-xsmall-height, 24px), + ), + small: ( + width: var(--ion-avatar-size-small-width, 32px), + height: var(--ion-avatar-size-small-height, 32px), + ), + medium: ( + width: var(--ion-avatar-size-medium-width, 40px), + height: var(--ion-avatar-size-medium-height, 40px), + ), + large: ( + width: var(--ion-avatar-size-large-width, 48px), + height: var(--ion-avatar-size-large-height, 48px), + ), + xlarge: ( + width: var(--ion-avatar-size-xlarge-width, 56px), + height: var(--ion-avatar-size-xlarge-height, 56px), + ), +); + +$badge-sizes: ( + small: ( + width: var(--ion-badge-size-small-empty-min-width), + height: var(--ion-badge-size-small-empty-height), + ), + medium: ( + width: var(--ion-badge-size-medium-empty-min-width), + height: var(--ion-badge-size-medium-empty-height), + ), + large: ( + width: var(--ion-badge-size-large-empty-min-width), + height: var(--ion-badge-size-large-empty-height), + ), +); + +// Avatar Sizes +@each $avatar-name, $avatar-tokens in $avatar-sizes { + $aw: map-get($avatar-tokens, width); + $ah: map-get($avatar-tokens, height); + + // Badge Sizes + @each $badge-name, $badge-tokens in $badge-sizes { + $bw: map-get($badge-tokens, width); + $bh: map-get($badge-tokens, height); + + // Top positioned empty badges + :host(.avatar-#{$avatar-name}) ::slotted(ion-badge.badge-#{$badge-name}.badge-vertical-top[vertical]:empty) { + @include badge-empty-translate($aw, $ah, $bw, $bh, top); + } + + // Bottom positioned empty badges + :host(.avatar-#{$avatar-name}) ::slotted(ion-badge.badge-#{$badge-name}.badge-vertical-bottom[vertical]:empty) { + @include badge-empty-translate($aw, $ah, $bw, $bh, bottom); + } + } + + // Top positioned badges with content + :host(.avatar-#{$avatar-name}) ::slotted(ion-badge.badge-vertical-top[vertical]:not(:empty)) { + @include badge-default-translate($aw, $ah, top); + } + + // Bottom positioned badges with content + :host(.avatar-#{$avatar-name}) ::slotted(ion-badge.badge-vertical-bottom[vertical]:not(:empty)) { + @include badge-default-translate($aw, $ah, bottom); + } +} diff --git a/core/src/components/avatar/avatar.common.scss b/core/src/components/avatar/avatar.common.scss index 3ed07d87612..3f5f402c409 100644 --- a/core/src/components/avatar/avatar.common.scss +++ b/core/src/components/avatar/avatar.common.scss @@ -1,4 +1,5 @@ @use "../../themes/mixins" as mixins; +@use "./avatar.badge"; // Avatar // -------------------------------------------------- @@ -25,300 +26,3 @@ overflow: hidden; } - -// Avatar Slotted Elements -// --------------------------------------------- - -// Badge Indicator inside Avatar xxsmall -:host(.avatar-xxsmall) ::slotted(ion-badge.badge-vertical-top:empty) { - @include mixins.transform( - translate( - var(--ion-avatar-size-xxsmall-badge-indicator-empty-top-translate-x), - var(--ion-avatar-size-xxsmall-badge-indicator-empty-top-translate-y) - ) - ); -} - -:host(.avatar-xxsmall) ::slotted(ion-badge.badge-vertical-bottom:empty) { - @include mixins.transform( - translate( - var(--ion-avatar-size-xxsmall-badge-indicator-empty-bottom-translate-x), - var(--ion-avatar-size-xxsmall-badge-indicator-empty-bottom-translate-y) - ) - ); -} - -:host(.avatar-xxsmall) ::slotted(ion-badge.badge-vertical-top:not(:empty)) { - @include mixins.position( - var(--ion-avatar-size-xxsmall-badge-indicator-default-top-position-top), - var(--ion-avatar-size-xxsmall-badge-indicator-default-top-position-end), - var(--ion-avatar-size-xxsmall-badge-indicator-default-top-position-bottom), - var(--ion-avatar-size-xxsmall-badge-indicator-default-top-position-start) - ); - @include mixins.transform( - translate( - var(--ion-avatar-size-xxsmall-badge-indicator-default-top-translate-x), - var(--ion-avatar-size-xxsmall-badge-indicator-default-top-translate-y) - ) - ); -} - -:host(.avatar-xxsmall) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) { - @include mixins.position( - var(--ion-avatar-size-xxsmall-badge-indicator-default-bottom-position-top), - var(--ion-avatar-size-xxsmall-badge-indicator-default-bottom-position-end), - var(--ion-avatar-size-xxsmall-badge-indicator-default-bottom-position-bottom), - var(--ion-avatar-size-xxsmall-badge-indicator-default-bottom-position-start) - ); - @include mixins.transform( - translate( - var(--ion-avatar-size-xxsmall-badge-indicator-default-bottom-translate-x), - var(--ion-avatar-size-xxsmall-badge-indicator-default-bottom-translate-y) - ) - ); -} - -// Badge Indicator inside Avatar xsmall -:host(.avatar-xsmall) ::slotted(ion-badge.badge-vertical-top:empty) { - @include mixins.transform( - translate( - var(--ion-avatar-size-xsmall-badge-indicator-empty-top-translate-x), - var(--ion-avatar-size-xsmall-badge-indicator-empty-top-translate-y) - ) - ); -} - -:host(.avatar-xsmall) ::slotted(ion-badge.badge-vertical-bottom:empty) { - @include mixins.transform( - translate( - var(--ion-avatar-size-xsmall-badge-indicator-empty-bottom-translate-x), - var(--ion-avatar-size-xsmall-badge-indicator-empty-bottom-translate-y) - ) - ); -} - -:host(.avatar-xsmall) ::slotted(ion-badge.badge-vertical-top:not(:empty)) { - @include mixins.position( - var(--ion-avatar-size-xsmall-badge-indicator-default-top-position-top), - var(--ion-avatar-size-xsmall-badge-indicator-default-top-position-end), - var(--ion-avatar-size-xsmall-badge-indicator-default-top-position-bottom), - var(--ion-avatar-size-xsmall-badge-indicator-default-top-position-start) - ); - @include mixins.transform( - translate( - var(--ion-avatar-size-xsmall-badge-indicator-default-top-translate-x), - var(--ion-avatar-size-xsmall-badge-indicator-default-top-translate-y) - ) - ); -} - -:host(.avatar-xsmall) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) { - @include mixins.position( - var(--ion-avatar-size-xsmall-badge-indicator-default-bottom-position-top), - var(--ion-avatar-size-xsmall-badge-indicator-default-bottom-position-end), - var(--ion-avatar-size-xsmall-badge-indicator-default-bottom-position-bottom), - var(--ion-avatar-size-xsmall-badge-indicator-default-bottom-position-start) - ); - @include mixins.transform( - translate( - var(--ion-avatar-size-xsmall-badge-indicator-default-bottom-translate-x), - var(--ion-avatar-size-xsmall-badge-indicator-default-bottom-translate-y) - ) - ); -} - -// Badge Indicator inside Avatar small -:host(.avatar-small) ::slotted(ion-badge.badge-vertical-top:empty) { - @include mixins.transform( - translate( - var(--ion-avatar-size-small-badge-indicator-empty-top-translate-x), - var(--ion-avatar-size-small-badge-indicator-empty-top-translate-y) - ) - ); -} - -:host(.avatar-small) ::slotted(ion-badge.badge-vertical-bottom:empty) { - @include mixins.transform( - translate( - var(--ion-avatar-size-small-badge-indicator-empty-bottom-translate-x), - var(--ion-avatar-size-small-badge-indicator-empty-bottom-translate-y) - ) - ); -} - -:host(.avatar-small) ::slotted(ion-badge.badge-vertical-top:not(:empty)) { - @include mixins.position( - var(--ion-avatar-size-small-badge-indicator-default-top-position-top), - var(--ion-avatar-size-small-badge-indicator-default-top-position-end), - var(--ion-avatar-size-small-badge-indicator-default-top-position-bottom), - var(--ion-avatar-size-small-badge-indicator-default-top-position-start) - ); - @include mixins.transform( - translate( - var(--ion-avatar-size-small-badge-indicator-default-top-translate-x), - var(--ion-avatar-size-small-badge-indicator-default-top-translate-y) - ) - ); -} - -:host(.avatar-small) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) { - @include mixins.position( - var(--ion-avatar-size-small-badge-indicator-default-bottom-position-top), - var(--ion-avatar-size-small-badge-indicator-default-bottom-position-end), - var(--ion-avatar-size-small-badge-indicator-default-bottom-position-bottom), - var(--ion-avatar-size-small-badge-indicator-default-bottom-position-start) - ); - @include mixins.transform( - translate( - var(--ion-avatar-size-small-badge-indicator-default-bottom-translate-x), - var(--ion-avatar-size-small-badge-indicator-default-bottom-translate-y) - ) - ); -} - -// Badge Indicator inside Avatar medium -:host(.avatar-medium) ::slotted(ion-badge.badge-vertical-top:empty) { - @include mixins.transform( - translate( - var(--ion-avatar-size-medium-badge-indicator-empty-top-translate-x), - var(--ion-avatar-size-medium-badge-indicator-empty-top-translate-y) - ) - ); -} - -:host(.avatar-medium) ::slotted(ion-badge.badge-vertical-bottom:empty) { - @include mixins.transform( - translate( - var(--ion-avatar-size-medium-badge-indicator-empty-bottom-translate-x), - var(--ion-avatar-size-medium-badge-indicator-empty-bottom-translate-y) - ) - ); -} - -:host(.avatar-medium) ::slotted(ion-badge.badge-vertical-top:not(:empty)) { - @include mixins.position( - var(--ion-avatar-size-medium-badge-indicator-default-top-position-top), - var(--ion-avatar-size-medium-badge-indicator-default-top-position-end), - var(--ion-avatar-size-medium-badge-indicator-default-top-position-bottom), - var(--ion-avatar-size-medium-badge-indicator-default-top-position-start) - ); - @include mixins.transform( - translate( - var(--ion-avatar-size-medium-badge-indicator-default-top-translate-x), - var(--ion-avatar-size-medium-badge-indicator-default-top-translate-y) - ) - ); -} - -:host(.avatar-medium) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) { - @include mixins.position( - var(--ion-avatar-size-medium-badge-indicator-default-bottom-position-top), - var(--ion-avatar-size-medium-badge-indicator-default-bottom-position-end), - var(--ion-avatar-size-medium-badge-indicator-default-bottom-position-bottom), - var(--ion-avatar-size-medium-badge-indicator-default-bottom-position-start) - ); - @include mixins.transform( - translate( - var(--ion-avatar-size-medium-badge-indicator-default-bottom-translate-x), - var(--ion-avatar-size-medium-badge-indicator-default-bottom-translate-y) - ) - ); -} - -// Badge Indicator inside Avatar large -:host(.avatar-large) ::slotted(ion-badge.badge-vertical-top:empty) { - @include mixins.transform( - translate( - var(--ion-avatar-size-large-badge-indicator-empty-top-translate-x), - var(--ion-avatar-size-large-badge-indicator-empty-top-translate-y) - ) - ); -} - -:host(.avatar-large) ::slotted(ion-badge.badge-vertical-bottom:empty) { - @include mixins.transform( - translate( - var(--ion-avatar-size-large-badge-indicator-empty-bottom-translate-x), - var(--ion-avatar-size-large-badge-indicator-empty-bottom-translate-y) - ) - ); -} - -:host(.avatar-large) ::slotted(ion-badge.badge-vertical-top:not(:empty)) { - @include mixins.position( - var(--ion-avatar-size-large-badge-indicator-default-top-position-top), - var(--ion-avatar-size-large-badge-indicator-default-top-position-end), - var(--ion-avatar-size-large-badge-indicator-default-top-position-bottom), - var(--ion-avatar-size-large-badge-indicator-default-top-position-start) - ); - @include mixins.transform( - translate( - var(--ion-avatar-size-large-badge-indicator-default-top-translate-x), - var(--ion-avatar-size-large-badge-indicator-default-top-translate-y) - ) - ); -} - -:host(.avatar-large) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) { - @include mixins.position( - var(--ion-avatar-size-large-badge-indicator-default-bottom-position-top), - var(--ion-avatar-size-large-badge-indicator-default-bottom-position-end), - var(--ion-avatar-size-large-badge-indicator-default-bottom-position-bottom), - var(--ion-avatar-size-large-badge-indicator-default-bottom-position-start) - ); - @include mixins.transform( - translate( - var(--ion-avatar-size-large-badge-indicator-default-bottom-translate-x), - var(--ion-avatar-size-large-badge-indicator-default-bottom-translate-y) - ) - ); -} - -// Badge Indicator inside Avatar xlarge -:host(.avatar-xlarge) ::slotted(ion-badge.badge-vertical-top:empty) { - @include mixins.transform( - translate( - var(--ion-avatar-size-xlarge-badge-indicator-empty-top-translate-x), - var(--ion-avatar-size-xlarge-badge-indicator-empty-top-translate-y) - ) - ); -} - -:host(.avatar-xlarge) ::slotted(ion-badge.badge-vertical-bottom:empty) { - @include mixins.transform( - translate( - var(--ion-avatar-size-xlarge-badge-indicator-empty-bottom-translate-x), - var(--ion-avatar-size-xlarge-badge-indicator-empty-bottom-translate-y) - ) - ); -} - -:host(.avatar-xlarge) ::slotted(ion-badge.badge-vertical-top:not(:empty)) { - @include mixins.position( - var(--ion-avatar-size-xlarge-badge-indicator-default-top-position-top), - var(--ion-avatar-size-xlarge-badge-indicator-default-top-position-end), - var(--ion-avatar-size-xlarge-badge-indicator-default-top-position-bottom), - var(--ion-avatar-size-xlarge-badge-indicator-default-top-position-start) - ); - @include mixins.transform( - translate( - var(--ion-avatar-size-xlarge-badge-indicator-default-top-translate-x), - var(--ion-avatar-size-xlarge-badge-indicator-default-top-translate-y) - ) - ); -} - -:host(.avatar-xlarge) ::slotted(ion-badge.badge-vertical-bottom:not(:empty)) { - @include mixins.position( - var(--ion-avatar-size-xlarge-badge-indicator-default-bottom-position-top), - var(--ion-avatar-size-xlarge-badge-indicator-default-bottom-position-end), - var(--ion-avatar-size-xlarge-badge-indicator-default-bottom-position-bottom), - var(--ion-avatar-size-xlarge-badge-indicator-default-bottom-position-start) - ); - @include mixins.transform( - translate( - var(--ion-avatar-size-xlarge-badge-indicator-default-bottom-translate-x), - var(--ion-avatar-size-xlarge-badge-indicator-default-bottom-translate-y) - ) - ); -} diff --git a/core/src/components/avatar/avatar.tsx b/core/src/components/avatar/avatar.tsx index da5ccb81707..a1a5a6cfcbd 100644 --- a/core/src/components/avatar/avatar.tsx +++ b/core/src/components/avatar/avatar.tsx @@ -54,14 +54,8 @@ export class Avatar implements ComponentInterface { } private getSize(): string | undefined { - const theme = getIonTheme(this); const { size } = this; - // TODO(ROU-10752): Remove theme check when sizes are defined for all themes. - if (theme !== 'ionic') { - return undefined; - } - if (size === undefined) { return 'medium'; } diff --git a/core/src/components/avatar/test/hint/index.html b/core/src/components/avatar/test/hint/index.html new file mode 100644 index 00000000000..765790530c2 --- /dev/null +++ b/core/src/components/avatar/test/hint/index.html @@ -0,0 +1,97 @@ + + + + + Avatar - Hint + + + + + + + + + + + + + + + Avatar - Hint + + + + + + + + + diff --git a/core/src/components/button/button.common.scss b/core/src/components/button/button.common.scss index b7e43f2e411..946b4e844b0 100644 --- a/core/src/components/button/button.common.scss +++ b/core/src/components/button/button.common.scss @@ -300,21 +300,15 @@ ion-ripple-effect { color: #{var(--ion-toolbar-background, var(--color))}; } -// Button Badge -// -------------------------------------------------- - .button-native ion-ripple-effect, .button-native::after { @include border-radius(inherit); } +// Button Slotted Elements +// --------------------------------------------- + +// Badge Indicator :host(.button-has-badge) .button-native { --overflow: visible; } - -:host ::slotted(ion-badge[vertical]:not(:empty)) { - display: flex; - - align-items: center; - justify-content: center; -} diff --git a/core/src/themes/ionic/default.tokens.ts b/core/src/themes/ionic/default.tokens.ts index 4f2b5272a85..75e7802ef1c 100644 --- a/core/src/themes/ionic/default.tokens.ts +++ b/core/src/themes/ionic/default.tokens.ts @@ -100,298 +100,6 @@ export const defaultTheme: DefaultTheme = { }, components: { - IonAvatar: { - size: { - xxsmall: { - badge: { - indicator: { - default: { - top: { - translate: { - x: '50%', - y: '-50%', - }, - - position: { - top: 'var(--ion-scaling-50)', - end: 'var(--ion-scaling-0)', - }, - }, - - bottom: { - translate: { - x: '100%', - y: '100%', - }, - - position: { - end: 'var(--ion-scaling-100)', - bottom: 'var(--ion-scaling-100)', - }, - }, - }, - - empty: { - top: { - translate: { - x: 'var(--ion-scaling-100)', - y: 'calc(var(--ion-scaling-100) * -1)', - }, - }, - - bottom: { - translate: { - x: 'var(--ion-scaling-100)', - y: 'var(--ion-scaling-100)', - }, - }, - }, - }, - }, - }, - - xsmall: { - badge: { - indicator: { - default: { - top: { - translate: { - x: '50%', - y: '-50%', - }, - - position: { - top: 'var(--ion-scaling-100)', - end: 'var(--ion-scaling-50)', - }, - }, - - bottom: { - translate: { - x: '50%', - y: '50%', - }, - - position: { - end: 'calc(var(--ion-scaling-50) * -1)', - bottom: 'calc(var(--ion-scaling-50) * -1)', - }, - }, - }, - - empty: { - top: { - translate: { - x: 'var(--ion-scaling-50)', - y: 'calc(var(--ion-scaling-50) * -1)', - }, - }, - - bottom: { - translate: { - x: 'var(--ion-scaling-0)', - y: 'var(--ion-scaling-100)', - }, - }, - }, - }, - }, - }, - - small: { - badge: { - indicator: { - default: { - top: { - translate: { - x: '50%', - y: '-50%', - }, - - position: { - top: 'var(--ion-scaling-150)', - end: 'var(--ion-scaling-0)', - }, - }, - - bottom: { - translate: { - x: '50%', - y: '50%', - }, - - position: { - end: 'var(--ion-scaling-50)', - bottom: 'var(--ion-scaling-50)', - }, - }, - }, - - empty: { - top: { - translate: { - x: 'var(--ion-scaling-50)', - y: 'calc(var(--ion-scaling-50) * -1)', - }, - }, - - bottom: { - translate: { - x: 'var(--ion-scaling-0)', - y: 'var(--ion-scaling-100)', - }, - }, - }, - }, - }, - }, - - medium: { - badge: { - indicator: { - default: { - top: { - translate: { - x: '50%', - y: '-50%', - }, - - position: { - top: 'var(--ion-scaling-150)', - end: 'var(--ion-scaling-0)', - }, - }, - - bottom: { - translate: { - x: '50%', - y: '50%', - }, - - position: { - end: 'var(--ion-scaling-50)', - bottom: 'var(--ion-scaling-50)', - }, - }, - }, - - empty: { - top: { - translate: { - x: 'var(--ion-scaling-50)', - y: 'calc(var(--ion-scaling-50) * -1)', - }, - }, - - bottom: { - translate: { - x: 'var(--ion-scaling-0)', - y: 'var(--ion-scaling-100)', - }, - }, - }, - }, - }, - }, - - large: { - badge: { - indicator: { - default: { - top: { - translate: { - x: '50%', - y: '-50%', - }, - - position: { - top: 'var(--ion-scaling-150)', - end: 'var(--ion-scaling-50)', - }, - }, - - bottom: { - translate: { - x: '50%', - y: '50%', - }, - - position: { - end: 'var(--ion-scaling-50)', - bottom: 'var(--ion-scaling-50)', - }, - }, - }, - - empty: { - top: { - translate: { - x: 'var(--ion-scaling-50)', - y: 'calc(var(--ion-scaling-50) * -1)', - }, - }, - - bottom: { - translate: { - x: 'var(--ion-scaling-0)', - y: 'var(--ion-scaling-100)', - }, - }, - }, - }, - }, - }, - - xlarge: { - badge: { - indicator: { - default: { - top: { - translate: { - x: '50%', - y: '-50%', - }, - - position: { - top: 'var(--ion-scaling-150)', - end: 'var(--ion-scaling-150)', - }, - }, - - bottom: { - translate: { - x: '50%', - y: '50%', - }, - - position: { - end: 'var(--ion-scaling-150)', - bottom: 'var(--ion-scaling-150)', - }, - }, - }, - - empty: { - top: { - translate: { - x: 'var(--ion-scaling-50)', - y: 'calc(var(--ion-scaling-50) * -1)', - }, - }, - - bottom: { - translate: { - x: 'var(--ion-scaling-0)', - y: 'var(--ion-scaling-100)', - }, - }, - }, - }, - }, - }, - }, - }, - IonBadge: { // Hues hue: { @@ -595,6 +303,16 @@ export const defaultTheme: DefaultTheme = { }, }, + IonButton: { + size: { + small: { + badge: { + indicator: {}, + }, + }, + }, + }, + IonChip: { margin: { top: 'var(--ion-spacing-0)',