diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 5d1eff6638c..34135a5a2d1 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -469,7 +469,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'; /** @@ -477,11 +477,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 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 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'; /** @@ -898,7 +898,7 @@ export namespace Components { */ "shape"?: IonChipShape; /** - * 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"?: IonChipSize; } @@ -6400,7 +6400,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'; /** @@ -6408,11 +6408,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 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 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'; /** @@ -6864,7 +6864,7 @@ declare namespace LocalJSX { */ "shape"?: IonChipShape; /** - * 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"?: IonChipSize; } 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 e24c047ad72..3f5f402c409 100644 --- a/core/src/components/avatar/avatar.common.scss +++ b/core/src/components/avatar/avatar.common.scss @@ -1,4 +1,5 @@ -@import "../../themes/mixins.scss"; +@use "../../themes/mixins" as mixins; +@use "./avatar.badge"; // Avatar // -------------------------------------------------- @@ -7,7 +8,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 +17,7 @@ ::slotted(ion-img), ::slotted(img) { - @include border-radius(var(--border-radius)); + @include mixins.border-radius(var(--border-radius)); width: 100%; height: 100%; 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/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/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/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..8df5f000de2 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 // -------------------------------------------------- @@ -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; @@ -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,11 +78,12 @@ --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) { - 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; } @@ -92,10 +94,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 +114,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}; @@ -160,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.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..dd22a1b2c31 --- /dev/null +++ b/core/src/components/badge/badge.scss @@ -0,0 +1,194 @@ +@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: inline-flex; + + align-items: center; + justify-content: 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-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); + + 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) { + 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); + + 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) { + 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); + + 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) { + 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; +} + +// TODO: remove from the tab-button.common +// In Tab Button +:host([vertical].in-tab-button) { + 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 369c2ad199b..28e1c258e98 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 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 `"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/badge/test/hint/index.html b/core/src/components/badge/test/hint/index.html index fba0d6e73ff..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/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/components/chip/chip.tsx b/core/src/components/chip/chip.tsx index 6d55dc8f439..cd4fb51fbe5 100644 --- a/core/src/components/chip/chip.tsx +++ b/core/src/components/chip/chip.tsx @@ -67,7 +67,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/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 e4d4407e2b6..75e7802ef1c 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'; @@ -21,6 +21,12 @@ export const defaultTheme: DefaultTheme = { formHighlight: true, components: { + IonBadge: { + hue: 'subtle', + shape: 'round', + size: 'small', + }, + IonChip: { fill: 'solid', hue: 'subtle', @@ -94,6 +100,219 @@ export const defaultTheme: DefaultTheme = { }, components: { + 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)', + }, + }, + }, + + IonButton: { + size: { + small: { + badge: { + indicator: {}, + }, + }, + }, + }, + IonChip: { margin: { top: 'var(--ion-spacing-0)', @@ -225,8 +444,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 }), }, }, }, @@ -247,11 +466,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', }, @@ -536,5 +755,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 f79dd62558b..ded3d5f2c4f 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, mix, dynamicFont } from '../../utils/theme'; +import { rgba, currentColor, clamp, 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'; @@ -23,6 +23,12 @@ export const defaultTheme: DefaultTheme = { config: { components: { + IonBadge: { + hue: 'bold', + shape: 'soft', + size: 'small', + }, + IonChip: { fill: 'solid', hue: 'bold', @@ -95,6 +101,200 @@ export const defaultTheme: DefaultTheme = { }, components: { + 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: { + 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)', + }, + + font: { + size: dynamicFont(global.root, 13), + weight: 'var(--ion-font-weight-bold)', + }, + + line: { + height: '1', + }, + }, + + 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)', + }, + + font: { + size: dynamicFont(global.root, 13), + weight: 'var(--ion-font-weight-bold)', + }, + + line: { + height: '1', + }, + }, + + 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)', + }, + + font: { + size: dynamicFont(global.root, 13), + weight: 'var(--ion-font-weight-bold)', + }, + + line: { + height: '1', + }, + }, + + 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 / global.root).toFixed(2)}rem`, + }, + + icon: { + width: 'var(--ion-scaling-xxxxs)', + height: 'var(--ion-scaling-xxxxs)', + }, + }, + }, + + tab: { + button: { + default: { + position: 'absolute', + }, + }, + }, + }, + }, + }, + IonChip: { margin: { top: 'var(--ion-spacing-xxs)', @@ -186,20 +386,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 +436,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 }), }, }, }, @@ -280,20 +480,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 +527,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 b978c59ef9b..e47991fb457 100644 --- a/core/src/themes/md/default.tokens.ts +++ b/core/src/themes/md/default.tokens.ts @@ -1,4 +1,4 @@ -import { rgba, currentColor, mix, dynamicFont } from '../../utils/theme'; +import { rgba, 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'; @@ -26,6 +26,12 @@ export const defaultTheme: DefaultTheme = { rippleEffect: true, components: { + IonBadge: { + hue: 'bold', + shape: 'soft', + size: 'small', + }, + IonChip: { fill: 'solid', hue: 'bold', @@ -98,6 +104,189 @@ export const defaultTheme: DefaultTheme = { }, components: { + IonBadge: { + font: { + size: dynamicFont(global.root, 13), + weight: 'var(--ion-font-weight-bold)', + }, + + // 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: { + soft: { + border: { + radius: 'var(--ion-radii-sm)', + }, + }, + + 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)', + }, + + font: { + size: dynamicFont(global.root, 13), + weight: 'var(--ion-font-weight-bold)', + }, + + line: { + height: '1', + }, + }, + + 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)', + }, + + font: { + size: dynamicFont(global.root, 13), + weight: 'var(--ion-font-weight-bold)', + }, + + line: { + height: '1', + }, + }, + + 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)', + }, + + font: { + size: dynamicFont(global.root, 13), + weight: 'var(--ion-font-weight-bold)', + }, + + line: { + height: '1', + }, + }, + + empty: { + height: 'var(--ion-scaling-350)', + + min: { + width: 'var(--ion-scaling-350)', + }, + }, + }, + }, + }, + + IonButton: { + badge: { + indicator: { + height: 'var(--ion-scaling-xxxs)', + + min: { + width: 'var(--ion-scaling-xxxs)', + }, + + line: { + height: 'var(--ion-scaling-xxs)', + }, + + font: { + size: `${(12 / global.root).toFixed(2)}rem`, + }, + + icon: { + width: 'var(--ion-scaling-xxxxs)', + height: 'var(--ion-scaling-xxxxs)', + }, + }, + }, + }, + IonChip: { margin: { top: 'var(--ion-spacing-xxs)', @@ -184,20 +373,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 }), }, }, }, @@ -233,22 +422,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 }), }, }, }, @@ -277,20 +466,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 }), }, }, }, @@ -324,25 +513,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 }), }, }, }, @@ -667,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 e7e8685cff9..fec9edb0d88 100644 --- a/core/src/themes/themes.interfaces.ts +++ b/core/src/themes/themes.interfaces.ts @@ -242,6 +242,7 @@ export type BaseTheme = { export type IonicConfig = IonicGlobalConfig & { components?: { + IonBadge?: any; IonChip?: IonChipConfig; IonSpinner?: IonSpinnerConfig; }; @@ -280,9 +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; diff --git a/core/src/utils/theme.ts b/core/src/utils/theme.ts index d0c098807cc..cd62a0c5b36 100644 --- a/core/src/utils/theme.ts +++ b/core/src/utils/theme.ts @@ -595,6 +595,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. * @@ -604,7 +609,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}`; @@ -612,14 +619,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})`; } /** @@ -629,7 +675,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})`;