From e729dea9555f34df2fc8045a7189c4b11ddd7026 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 15 Dec 2025 08:27:34 -0800 Subject: [PATCH 01/13] feat(chip): add recipe and variables --- core/src/components.d.ts | 10 +- core/src/components/chip/chip.base.scss | 222 +++++++++++++++++++ core/src/components/chip/chip.base.vars.scss | 149 +++++++++++++ core/src/components/chip/chip.tsx | 29 +-- core/src/global/ionic-global.ts | 10 +- core/src/themes/ios/default.tokens.ts | 128 +++++++++++ core/src/themes/md/default.tokens.ts | 127 +++++++++++ core/src/themes/mixins.scss | 20 ++ core/src/themes/themes.interfaces.ts | 131 ++++++++++- core/src/utils/test/theme.spec.ts | 14 +- core/src/utils/theme.ts | 184 +++++++++++---- 11 files changed, 945 insertions(+), 79 deletions(-) create mode 100644 core/src/components/chip/chip.base.scss create mode 100644 core/src/components/chip/chip.base.vars.scss diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 5d39ba36505..f5ebc5b6328 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -872,7 +872,7 @@ export namespace Components { */ "disabled": boolean; /** - * Set to `"bold"` for a chip with vibrant, bold colors or to `"subtle"` for a chip with muted, subtle colors. Only applies to the `ionic` theme. + * Set to `"bold"` for a chip with vibrant, bold colors or to `"subtle"` for a chip with muted, subtle colors. Defaults to `"subtle"`. * @default 'subtle' */ "hue"?: 'bold' | 'subtle'; @@ -891,8 +891,9 @@ export namespace Components { "shape"?: 'soft' | 'round' | 'rectangular'; /** * Set to `"small"` for a chip with less height and padding. Defaults to `"large"` for the ionic theme, and undefined for all other themes. + * @default 'medium' */ - "size"?: 'small' | 'large'; + "size"?: 'small' | 'medium' | 'large'; /** * The theme determines the visual appearance of the component. */ @@ -6842,7 +6843,7 @@ declare namespace LocalJSX { */ "disabled"?: boolean; /** - * Set to `"bold"` for a chip with vibrant, bold colors or to `"subtle"` for a chip with muted, subtle colors. Only applies to the `ionic` theme. + * Set to `"bold"` for a chip with vibrant, bold colors or to `"subtle"` for a chip with muted, subtle colors. Defaults to `"subtle"`. * @default 'subtle' */ "hue"?: 'bold' | 'subtle'; @@ -6861,8 +6862,9 @@ declare namespace LocalJSX { "shape"?: 'soft' | 'round' | 'rectangular'; /** * Set to `"small"` for a chip with less height and padding. Defaults to `"large"` for the ionic theme, and undefined for all other themes. + * @default 'medium' */ - "size"?: 'small' | 'large'; + "size"?: 'small' | 'medium' | 'large'; /** * The theme determines the visual appearance of the component. */ diff --git a/core/src/components/chip/chip.base.scss b/core/src/components/chip/chip.base.scss new file mode 100644 index 00000000000..ff9397b509b --- /dev/null +++ b/core/src/components/chip/chip.base.scss @@ -0,0 +1,222 @@ +@use "../../themes/mixins" as mixins; +@use "../../themes/functions.color" as colors; +@use "./chip.base.vars.scss" as vars; + +// Chip: Common Styles +// -------------------------------------------------- + +:host { + /** + * @prop --background: Background of the chip + * @prop --border-radius: Border radius of the chip + * @prop --color: Color of the chip + * @prop --focus-ring-color: Color of the focus ring + * @prop --focus-ring-width: Width of the focus ring + */ + --focus-ring-color: #{vars.$chip-focus-ring-color}; + --focus-ring-width: #{vars.$chip-focus-ring-width}; + + @include mixins.font-smoothing(); + @include mixins.border-radius(var(--border-radius)); + @include mixins.margin(vars.$chip-margin); + @include mixins.padding(vars.$chip-padding-vertical, vars.$chip-padding-horizontal); + + display: inline-flex; + position: relative; + + align-items: center; + justify-content: center; + + background: var(--background); + color: var(--color); + + line-height: vars.$chip-line-height; + + cursor: pointer; + overflow: hidden; + vertical-align: middle; + box-sizing: border-box; + + gap: vars.$chip-gap; +} + +// Chip Sizes +// --------------------------------------------- + +:host(.chip-small) { + min-height: vars.$chip-size-small-height; + + font-size: vars.$chip-size-small-font-size; +} + +:host(.chip-medium) { + min-height: vars.$chip-size-medium-height; + + font-size: vars.$chip-size-medium-font-size; +} + +:host(.chip-large) { + min-height: vars.$chip-size-large-height; + + font-size: vars.$chip-size-large-font-size; +} + +// Chip Shapes +// --------------------------------------------- + +:host(.chip-soft) { + --border-radius: #{vars.$chip-border-radius-soft}; +} + +:host(.chip-round) { + --border-radius: #{vars.$chip-border-radius-round}; +} + +:host(.chip-rectangular) { + --border-radius: #{vars.$chip-border-radius-rectangular}; +} + +// Chip Hues +// --------------------------------------------- + +// Bold +:host(.chip-bold) { + --background: #{vars.$chip-hue-bold-bg}; + --color: #{vars.$chip-hue-bold-color}; +} + +:host(.chip-bold.chip-outline) { + border-color: #{vars.$chip-hue-bold-outline-border-color}; +} + +// Subtle +:host(.chip-subtle) { + --background: #{vars.$chip-hue-subtle-bg}; + --color: #{vars.$chip-hue-subtle-color}; +} + +:host(.chip-subtle.chip-outline) { + border-color: #{vars.$chip-hue-subtle-outline-border-color}; +} + +// Chip Colors +// --------------------------------------------- + +// Bold +:host(.ion-color.chip-bold) { + background: colors.current-color(base, vars.$chip-hue-bold-semantic-bg-alpha); + color: vars.$chip-hue-bold-semantic-color; +} + +:host(.ion-color.chip-bold.chip-outline) { + border-color: vars.$chip-hue-bold-semantic-outline-border-color; +} + +// Subtle +:host(.ion-color.chip-subtle) { + background: colors.current-color(base, $subtle: true); + color: colors.current-color(contrast, $subtle: true); +} + +:host(.ion-color.chip-subtle.chip-outline) { + border-color: colors.current-color(shade, $subtle: true); +} + +// Outline Chip +// --------------------------------------------- + +:host(.chip-outline) { + border-width: vars.$chip-outline-border-width; + border-style: solid; +} + +:host(.chip-outline), +:host(.chip-outline.ion-color) { + background: vars.$chip-outline-bg; +} + +// Chip States +// --------------------------------------------- + +// Disabled +:host(.chip-disabled) { + cursor: default; + opacity: vars.$chip-state-disabled-opacity; + pointer-events: none; +} + +// Focus +:host(.ion-focused) { + --background: #{vars.$chip-focus-bg}; + + @include mixins.focused-state(var(--focus-ring-width), $color: var(--focus-ring-color)); +} + +:host(.ion-focused.ion-color) { + background: vars.$chip-focus-semantic-bg; +} + +:host(.ion-focused.chip-outline:not(.ion-color)) { + background: vars.$chip-outline-focus-bg; +} + +// Activated +:host(.ion-activated) { + --background: #{vars.$chip-activated-bg}; +} + +:host(.ion-activated.ion-color) { + background: vars.$chip-activated-semantic-bg; +} + +// Hover +@media (any-hover: hover) { + :host(:hover) { + --background: #{vars.$chip-hover-bg}; + } + + :host(.ion-color:hover) { + background: vars.$chip-hover-semantic-bg; + } + + :host(.chip-outline:not(.ion-color):hover) { + background: vars.$chip-outline-hover-bg; + } +} + +// Chip Slotted Elements +// --------------------------------------------- + +// Icon +::slotted(ion-icon) { + font-size: vars.$chip-icon-size; +} + +:host(:not(.ion-color)) ::slotted(ion-icon) { + color: vars.$chip-icon-color; +} + +::slotted(ion-icon:first-child) { + @include mixins.margin(vars.$chip-icon-first-child-margin, vars.$chip-icon-first-child-margin-end, $start: vars.$chip-icon-first-child-margin); +} + +::slotted(ion-icon:last-child) { + @include mixins.margin(vars.$chip-icon-last-child-margin, $start: vars.$chip-icon-last-child-margin-start); +} + +// Avatar +::slotted(ion-avatar) { + flex-shrink: 0; + + width: vars.$chip-avatar-size; + height: vars.$chip-avatar-size; +} + +::slotted(ion-avatar:first-child) { + @include mixins.margin(vars.$chip-avatar-first-child-margin-vertical, $end: vars.$chip-avatar-first-child-margin-end, $start: vars.$chip-avatar-first-child-margin-start); +} + +::slotted(ion-avatar:last-child) { + @include mixins.margin(vars.$chip-avatar-last-child-margin-vertical, $end: vars.$chip-avatar-last-child-margin-end, $start: vars.$chip-avatar-last-child-margin-start); +} + diff --git a/core/src/components/chip/chip.base.vars.scss b/core/src/components/chip/chip.base.vars.scss new file mode 100644 index 00000000000..47f578de860 --- /dev/null +++ b/core/src/components/chip/chip.base.vars.scss @@ -0,0 +1,149 @@ +/// Chip Variables +/// --------------------------------------------- + +/// @prop - Margin of the chip +$chip-margin: var(--ion-chip-margin); + +/// @prop - Vertical padding of the chip +$chip-padding-vertical: var(--ion-chip-padding-vertical); + +/// @prop - Horizontal padding of the chip +$chip-padding-horizontal: var(--ion-chip-padding-horizontal); + +/// @prop - Gap between chip elements +$chip-gap: var(--ion-chip-gap); + +/// @prop - Line height of the chip +$chip-line-height: var(--ion-chip-line-height); + +/// @prop - Opacity of disabled chip +$chip-state-disabled-opacity: var(--ion-chip-state-disabled-opacity); + +/// @prop - Size small: Height of the chip +$chip-size-small-height: var(--ion-chip-size-small-height); + +/// @prop - Size small: Font size of the chip +$chip-size-small-font-size: var(--ion-chip-size-small-font-size); + +/// @prop - Size medium: Height of the chip +$chip-size-medium-height: var(--ion-chip-size-medium-height); + +/// @prop - Size medium: Font size of the chip +$chip-size-medium-font-size: var(--ion-chip-size-medium-font-size); + +/// @prop - Size large: Height of the chip +$chip-size-large-height: var(--ion-chip-size-large-height); + +/// @prop - Size large: Font size of the chip +$chip-size-large-font-size: var(--ion-chip-size-large-font-size); + +/// @prop - Soft chip border radius +$chip-border-radius-soft: var(--ion-chip-shape-soft-border-radius); + +/// @prop - Round chip border radius +$chip-border-radius-round: var(--ion-chip-shape-round-border-radius); + +/// @prop - Rectangular chip border radius +$chip-border-radius-rectangular: var(--ion-chip-shape-rectangular-border-radius); + +/// @prop - Subtle chip background color +$chip-hue-subtle-bg: var(--ion-chip-hue-subtle-bg); + +/// @prop - Subtle chip color +$chip-hue-subtle-color: var(--ion-chip-hue-subtle-color); + +/// @prop - Outline subtle chip border color +$chip-hue-subtle-outline-border-color: var(--ion-chip-hue-subtle-outline-border-color); + +/// @prop - Bold chip background color +$chip-hue-bold-bg: var(--ion-chip-hue-bold-bg); + +/// @prop - Bold chip color +$chip-hue-bold-color: var(--ion-chip-hue-bold-color); + +/// @prop - Outline bold chip border color +$chip-hue-bold-outline-border-color: var(--ion-chip-hue-bold-outline-border-color); + +/// @prop - Bold chip background alpha of semantic colors +$chip-hue-bold-semantic-bg-alpha: var(--ion-chip-hue-bold-semantic-bg-alpha); // only native uses this + +/// @prop - Bold chip color for semantic colors +$chip-hue-bold-semantic-color: var(--ion-chip-hue-bold-semantic-color); + +/// @prop - Outline bold chip border color for semantic colors +$chip-hue-bold-semantic-outline-border-color: var(--ion-chip-hue-bold-semantic-outline-border-color); + +/// @prop - Outline border width +$chip-outline-border-width: var(--ion-chip-variant-outline-border-width); + +/// @prop - Outline chip background color +$chip-outline-bg: var(--ion-chip-variant-outline-bg); + +/// @prop - Focus ring color +$chip-focus-ring-color: var(--ion-chip-state-focus-ring-color); + +/// @prop - Focus ring width +$chip-focus-ring-width: var(--ion-chip-state-focus-ring-width); + +/// @prop - Focus ring background color +$chip-focus-bg: var(--ion-chip-state-focus-bg); + +/// @prop - Focus background color for semantic colors +$chip-focus-semantic-bg: var(--ion-chip-state-focus-semantic-bg); + +/// @prop - Outline focus background color +$chip-outline-focus-bg: var(--ion-chip-state-focus-outline-bg); + +/// @prop - Activated background color +$chip-activated-bg: var(--ion-chip-state-activated-bg); + +/// @prop - Activated background color for semantic colors +$chip-activated-semantic-bg: var(--ion-chip-state-activated-semantic-bg); + +/// @prop - Hover background color +$chip-hover-bg: var(--ion-chip-state-hover-bg); + +/// @prop - Hover background color for semantic colors +$chip-hover-semantic-bg: var(--ion-chip-state-hover-semantic-bg); + +/// @prop - Outline hover background color +$chip-outline-hover-bg: var(--ion-chip-state-hover-outline-bg); + +/// @prop - Icon size +$chip-icon-size: var(--ion-chip-icon-size); + +/// @prop - Icon color +$chip-icon-color: var(--ion-chip-icon-color); + +/// @prop - Icon margin for first child +$chip-icon-first-child-margin: var(--ion-chip-icon-first-child-margin); + +/// @prop - Icon margin end for first child +$chip-icon-first-child-margin-end: var(--ion-chip-icon-first-child-margin-end); + +/// @prop - Icon margin for last child +$chip-icon-last-child-margin: var(--ion-chip-icon-last-child-margin); + +/// @prop - Icon margin start for last child +$chip-icon-last-child-margin-start: var(--ion-chip-icon-last-child-margin-start); + +/// @prop - Avatar size +$chip-avatar-size: var(--ion-chip-avatar-size); + +/// @prop - Avatar margin vertical for first child +$chip-avatar-first-child-margin-vertical: var(--ion-chip-avatar-first-child-margin-vertical); + +/// @prop - Avatar margin horizontal start for first child +$chip-avatar-first-child-margin-start: var(--ion-chip-avatar-first-child-margin-start); + +/// @prop - Avatar margin horizontal end for first child +$chip-avatar-first-child-margin-end: var(--ion-chip-avatar-first-child-margin-end); + +/// @prop - Avatar margin vertical for last child +$chip-avatar-last-child-margin-vertical: var(--ion-chip-avatar-last-child-margin-vertical); + +/// @prop - Avatar margin start for last child +$chip-avatar-last-child-margin-start: var(--ion-chip-avatar-last-child-margin-start); + +/// @prop - Avatar margin end for last child +$chip-avatar-last-child-margin-end: var(--ion-chip-avatar-last-child-margin-end); \ No newline at end of file diff --git a/core/src/components/chip/chip.tsx b/core/src/components/chip/chip.tsx index bbd30e823cb..edd5d980ea4 100644 --- a/core/src/components/chip/chip.tsx +++ b/core/src/components/chip/chip.tsx @@ -1,6 +1,5 @@ import type { ComponentInterface } from '@stencil/core'; import { Component, Host, Prop, h } from '@stencil/core'; -import { printIonWarning } from '@utils/logging'; import { createColorClasses } from '@utils/theme'; import { getIonTheme } from '../../global/ionic-global'; @@ -12,11 +11,7 @@ import type { Color } from '../../interface'; */ @Component({ tag: 'ion-chip', - styleUrls: { - ios: 'chip.ios.scss', - md: 'chip.md.scss', - ionic: 'chip.ionic.scss', - }, + styleUrl: 'chip.base.scss', shadow: true, }) export class Chip implements ComponentInterface { @@ -41,7 +36,7 @@ export class Chip implements ComponentInterface { * Set to `"bold"` for a chip with vibrant, bold colors or to `"subtle"` for * a chip with muted, subtle colors. * - * Only applies to the `ionic` theme. + * Defaults to `"subtle"`. */ @Prop() hue?: 'bold' | 'subtle' = 'subtle'; @@ -71,27 +66,11 @@ export class Chip implements ComponentInterface { * * Defaults to `"large"` for the ionic theme, and undefined for all other themes. */ - @Prop() size?: 'small' | 'large'; - - private getSize() { - const theme = getIonTheme(this); - const { size } = this; - - if (theme === 'ionic') { - return size !== undefined ? size : 'large'; - // TODO(ROU-10695): remove the size !== undefined when we add support for - // the `ios` and `md` themes. - } else if (size !== undefined) { - printIonWarning(`The "${size}" size is not supported in the ${theme} theme.`); - } - - return undefined; - } + @Prop() size?: 'small' | 'medium' | 'large' = 'medium'; render() { - const { hue } = this; + const { hue, size } = this; const theme = getIonTheme(this); - const size = this.getSize(); const shape = this.getShape(); return ( diff --git a/core/src/global/ionic-global.ts b/core/src/global/ionic-global.ts index 4d57b33a22d..bb6bb25f8ad 100644 --- a/core/src/global/ionic-global.ts +++ b/core/src/global/ionic-global.ts @@ -1,6 +1,6 @@ import { Build, getMode, setMode, getElement } from '@stencil/core'; import { printIonWarning } from '@utils/logging'; -import { applyGlobalTheme, getCustomTheme } from '@utils/theme'; +import { applyComponentsTheme, applyGlobalTheme, getCustomTheme } from '@utils/theme'; import type { IonicConfig, Mode, Theme } from '../interface'; import { defaultTheme as baseTheme } from '../themes/base/default.tokens'; @@ -152,9 +152,17 @@ export const initialize = (userConfig: IonicConfig = {}) => { // Apply base theme, or combine with custom theme if provided if (customTheme) { const combinedTheme = applyGlobalTheme(baseTheme, customTheme); + // Component styles must be applied after global styles in order + // to ensure CSS variables are available for components + // like the semantic colors (e.g., --ion-color-shade) + applyComponentsTheme(combinedTheme); config.set('customTheme', combinedTheme); } else { applyGlobalTheme(baseTheme); + // Component styles must be applied after global styles in order + // to ensure CSS variables are available for components + // like the semantic colors (e.g., --ion-color-shade) + applyComponentsTheme(baseTheme); config.set('customTheme', baseTheme); } diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts index 266191a1cb8..268f051d4dd 100644 --- a/core/src/themes/ios/default.tokens.ts +++ b/core/src/themes/ios/default.tokens.ts @@ -1,9 +1,21 @@ +import { rgba, currentColor, clamp } from '../../utils/theme'; import { defaultTheme as baseDefaultTheme } from '../base/default.tokens'; import type { DefaultTheme } from '../themes.interfaces'; import { darkTheme } from './dark.tokens'; import { lightTheme } from './light.tokens'; +const colors = { + backgroundColor: 'var(--ion-background-color, #fff)', + backgroundColorRgb: 'var(--ion-background-color-rgb, 255, 255, 255)', + textColor: 'var(--ion-text-color, #000)', + textColorRgb: 'var(--ion-text-color-rgb, 0, 0, 0)', +} + +const fontSizes = { + chipBase: 14, +} + export const defaultTheme: DefaultTheme = { ...baseDefaultTheme, @@ -71,4 +83,120 @@ export const defaultTheme: DefaultTheme = { xxxl: 'var(--ion-radii-500)', xxxxl: 'var(--ion-radii-full)', }, + + + components: { + IonChip: { + margin: '4px', + paddingVertical: '6px', + paddingHorizontal: '12px', + + // Sizes + size: { + small: { + height: '24px', + fontSize: clamp('12px', `${(fontSizes.chipBase - 2) / 16}rem`, '20px'), + }, + medium: { + height: '32px', + fontSize: clamp('13px', `${fontSizes.chipBase / 16}rem`, '22px'), + }, + large: { + height: '32px', + fontSize: clamp('14px', `${(fontSizes.chipBase + 2) / 16}rem`, '24px'), + }, + }, + + // States + state: { + disabled: { + opacity: '0.4', + }, + focus: { + bg: rgba(colors.textColorRgb, 0.16), + semanticBg: currentColor('base', 0.12), + outlineBg: rgba(colors.textColorRgb, 0.04), + }, + activated: { + bg: rgba(colors.textColorRgb, 0.2), + semanticBg: currentColor('base', 0.16), + }, + hover: { + bg: rgba(colors.textColorRgb, 0.16), + semanticBg: currentColor('base', 0.12), + outlineBg: rgba(colors.textColorRgb, 0.04), + }, + }, + + // Shapes + shape: { + soft: { + borderRadius: 'var(--ion-radii-250)', + }, + round: { + borderRadius: 'var(--ion-radii-full)', + }, + rectangular: { + borderRadius: 'var(--ion-radii-0)', + }, + }, + + // Hues + hue: { + bold: { + bg: rgba(colors.textColorRgb, 0.12), + color: rgba(colors.textColorRgb, 0.87), + + outline: { + borderColor: rgba(colors.textColorRgb, 0.32), + }, + + // Any of the semantic colors like primary, secondary, etc. + semantic: { + bgAlpha: '0.08', + color: currentColor('shade'), + + outline: { + borderColor: currentColor('base', 0.32), + } + }, + }, + subtle: { + bg: rgba(colors.textColorRgb, 0.04), + color: rgba(colors.textColorRgb, 0.87), + + outline: { + borderColor: rgba(colors.textColorRgb, 0.32), + }, + }, + }, + + // Variants + variant: { + outline: { + borderWidth: '1px', + bg: 'transparent', + }, + }, + + icon: { + size: `${20 / fontSizes.chipBase}em`, + color: rgba(colors.textColorRgb, 0.54), + firstChildMargin: '-4px', + firstChildMarginEnd: '8px', + lastChildMargin: '-4px', + lastChildMarginStart: '8px', + }, + + avatar: { + size: `${24 / fontSizes.chipBase}em`, + firstChildMarginVertical: '-4px', + firstChildMarginStart: '-8px', + firstChildMarginEnd: '8px', + lastChildMarginVertical: '-4px', + lastChildMarginStart: '8px', + lastChildMarginEnd: '-8px', + }, + }, + } }; diff --git a/core/src/themes/md/default.tokens.ts b/core/src/themes/md/default.tokens.ts index 7dcff7c0445..a491edc109f 100644 --- a/core/src/themes/md/default.tokens.ts +++ b/core/src/themes/md/default.tokens.ts @@ -1,9 +1,21 @@ +import { rgba, currentColor } from '../../utils/theme'; import { defaultTheme as baseDefaultTheme } from '../base/default.tokens'; import type { DefaultTheme } from '../themes.interfaces'; import { darkTheme } from './dark.tokens'; import { lightTheme } from './light.tokens'; +const colors = { + backgroundColor: 'var(--ion-background-color, #fff)', + backgroundColorRgb: 'var(--ion-background-color-rgb, 255, 255, 255)', + textColor: 'var(--ion-text-color, #000)', + textColorRgb: 'var(--ion-text-color-rgb, 0, 0, 0)', +} + +const fontSizes = { + chipBase: 14, +} + export const defaultTheme: DefaultTheme = { ...baseDefaultTheme, @@ -76,4 +88,119 @@ export const defaultTheme: DefaultTheme = { xxxl: 'var(--ion-radii-900)', xxxxl: 'var(--ion-radii-full)', }, + + components: { + IonChip: { + margin: '4px', + paddingVertical: '6px', + paddingHorizontal: '12px', + + // Sizes + size: { + small: { + height: '24px', + fontSize: `${(fontSizes.chipBase - 2) / 16}rem`, + }, + medium: { + height: '32px', + fontSize: `${fontSizes.chipBase / 16}rem`, + }, + large: { + height: '32px', + fontSize: `${(fontSizes.chipBase + 2) / 16}rem`, + }, + }, + + // States + state: { + disabled: { + opacity: '0.4', + }, + focus: { + bg: rgba(colors.textColorRgb, 0.16), + semanticBg: currentColor('base', 0.12), + outlineBg: rgba(colors.textColorRgb, 0.04), + }, + activated: { + bg: rgba(colors.textColorRgb, 0.2), + semanticBg: currentColor('base', 0.16), + }, + hover: { + bg: rgba(colors.textColorRgb, 0.16), + semanticBg: currentColor('base', 0.12), + outlineBg: rgba(colors.textColorRgb, 0.04), + }, + }, + + // Shapes + shape: { + soft: { + borderRadius: 'var(--ion-radii-200)', + }, + round: { + borderRadius: 'var(--ion-radii-full)', + }, + rectangular: { + borderRadius: 'var(--ion-radii-0)', + }, + }, + + // Hues + hue: { + bold: { + bg: rgba(colors.textColorRgb, 0.12), + color: rgba(colors.textColorRgb, 0.87), + + outline: { + borderColor: rgba(colors.textColorRgb, 0.32), + }, + + // Any of the semantic colors like primary, secondary, etc. + semantic: { + bgAlpha: '0.08', + color: currentColor('shade'), + + outline: { + borderColor: currentColor('base', 0.32), + } + }, + }, + subtle: { + bg: rgba(colors.textColorRgb, 0.04), + color: rgba(colors.textColorRgb, 0.87), + + outline: { + borderColor: rgba(colors.textColorRgb, 0.32), + }, + }, + }, + + // Variants + variant: { + outline: { + borderWidth: '1px', + bg: 'transparent', + }, + }, + + icon: { + size: `${20 / fontSizes.chipBase}em`, + color: rgba(colors.textColorRgb, 0.54), + firstChildMargin: '-4px', + firstChildMarginEnd: '8px', + lastChildMargin: '-4px', + lastChildMarginStart: '8px', + }, + + avatar: { + size: `${24 / fontSizes.chipBase}em`, + firstChildMarginVertical: '-4px', + firstChildMarginStart: '-8px', + firstChildMarginEnd: '8px', + lastChildMarginVertical: '-4px', + lastChildMarginStart: '8px', + lastChildMarginEnd: '-8px', + }, + }, + } }; diff --git a/core/src/themes/mixins.scss b/core/src/themes/mixins.scss index d21c89d2e9f..c9eba257a8c 100644 --- a/core/src/themes/mixins.scss +++ b/core/src/themes/mixins.scss @@ -1,4 +1,5 @@ @use "./functions.string" as string; +@use 'sass:meta'; /** * A heuristic that applies CSS to tablet @@ -609,3 +610,22 @@ } } } + +// Focused mixin to be used on regular elements +// +// ex: :host(.ion-focused) .toggle-icon { +// @include mixins.focused-state(); +// } +// +// -------------------------------------------------- +@mixin focused-state($width: null, $style: solid, $color: null, $addOffset: true) { + @if $width == null or $color == null { + outline: none; + } @else { + outline: $width $style $color; + + @if $addOffset { + outline-offset: $width; + } + } +} diff --git a/core/src/themes/themes.interfaces.ts b/core/src/themes/themes.interfaces.ts index 6e5e8157e29..ab94c3a99a2 100644 --- a/core/src/themes/themes.interfaces.ts +++ b/core/src/themes/themes.interfaces.ts @@ -211,11 +211,7 @@ export type BaseTheme = { }; // COMPONENT OVERRIDES - components?: { - [key: string]: { - [key: string]: string; - }; - }; + components?: Components; // COLOR TOKENS color?: { @@ -261,3 +257,128 @@ export type DefaultTheme = BaseTheme & { config?: IonicConfig; }; + +type Components = { + IonChip?: { + margin: string | number; + paddingVertical: string | number; + paddingHorizontal: string | number; + gap?: string | number; + lineHeight?: string | number; + + // Sizes + size: { + small: { + height: string | number; + fontSize: string | number; + }; + medium: { + height: string | number; + fontSize?: string | number; + }; + large: { + height: string | number; + fontSize: string | number; + }; + }; + + // States + state: { + disabled: { + opacity: string | number; + }; + focus: { + ringColor?: string; + ringWidth?: string | number; + bg: string; + semanticBg: string; + outlineBg: string; + }; + activated: { + bg: string; + semanticBg: string; + }; + hover: { + bg: string; + semanticBg: string; + outlineBg: string; + }; + }; + + // Shapes + shape: { + soft: { + borderRadius: string | number; + }; + round: { + borderRadius: string | number; + }; + rectangular: { + borderRadius: string | number; + }; + }; + + // Hues + hue: { + bold: { + bg: string; + color: string; + + outline: { + borderColor: string; + } + + // Any of the semantic colors like primary, secondary, etc. + semantic: { + bgAlpha: string; + color: string; + + outline: { + borderColor: string; + } + }; + }; + subtle: { + bg: string; + color: string; + + outline: { + borderColor: string; + } + }; + }; + + // Variants + variant: { + outline: { + borderWidth: string | number; + bg: string; + }; + }; + + icon: { + size: string | number; + color: string; + firstChildMargin: string | number; + firstChildMarginEnd: string | number; + lastChildMargin: string | number; + lastChildMarginStart: string | number; + }; + + avatar: { + size: string | number; + firstChildMarginVertical: string | number; + firstChildMarginStart: string | number; + firstChildMarginEnd: string | number; + lastChildMarginVertical: string | number; + lastChildMarginStart: string | number; + lastChildMarginEnd: string | number; + }; + }; + + IonCard?: any; + IonItem?: any; + IonTabBar?: any; + IonModal?: any; + IonToolbar?: any; +}; \ No newline at end of file diff --git a/core/src/utils/test/theme.spec.ts b/core/src/utils/test/theme.spec.ts index 9c68d95510d..8edb5a4d6ac 100644 --- a/core/src/utils/test/theme.spec.ts +++ b/core/src/utils/test/theme.spec.ts @@ -5,7 +5,7 @@ import { CardContent } from '../../components/card-content/card-content'; import { Chip } from '../../components/chip/chip'; import { generateColorClasses, - generateComponentThemeCSS, + generateComponentsThemeCSS, generateCSSVars, generateGlobalThemeCSS, getClassList, @@ -557,10 +557,11 @@ describe('generateGlobalThemeCSS', () => { }); }); -describe('generateComponentThemeCSS', () => { +describe('generateComponentsThemeCSS', () => { it('should generate component theme CSS for a given theme', () => { - const IonChip = { - hue: { + const components = { + IonChip: { + hue: { subtle: { bg: 'red', color: 'white', @@ -572,12 +573,13 @@ describe('generateComponentThemeCSS', () => { borderColor: 'black', }, }, + } }; - const css = generateComponentThemeCSS(IonChip, 'chip').replace(/\s/g, ''); + const css = generateComponentsThemeCSS(components).replace(/\s/g, ''); const expectedCSS = ` - :host(.chip-themed) { + :root ion-chip { --ion-chip-hue-subtle-bg: red; --ion-chip-hue-subtle-color: white; --ion-chip-hue-subtle-border-color: black; diff --git a/core/src/utils/theme.ts b/core/src/utils/theme.ts index aeab9823ec2..8c37d4fd044 100644 --- a/core/src/utils/theme.ts +++ b/core/src/utils/theme.ts @@ -81,10 +81,7 @@ export const generateCSSVars = (theme: any, prefix: string = CSS_PROPS_PREFIX): return []; } - // if key is camelCase, convert to kebab-case - if (key.match(/([a-z])([A-Z])/g)) { - key = key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); - } + key = convertToKebabCase(key); // Do not generate CSS variables for excluded keys const excludedKeys = ['name', 'enabled', 'config']; @@ -264,13 +261,8 @@ export const injectCSS = (css: string, target: Element | ShadowRoot = document.h * @returns The generated CSS string */ export const generateGlobalThemeCSS = (theme: any): string => { - if (typeof theme !== 'object' || Array.isArray(theme)) { - console.warn('generateGlobalThemeCSS: Invalid theme object provided', theme); - return ''; - } - - if (Object.keys(theme).length === 0) { - console.warn('generateGlobalThemeCSS: Empty theme object provided'); + const themeValidity = checkThemeValidity(theme, 'generateGlobalThemeCSS'); + if (!themeValidity) { return ''; } @@ -361,14 +353,29 @@ export const applyGlobalTheme = (baseTheme: any, userTheme?: any): any => { * @param componentName The component name without any prefixes (e.g., 'chip') * @returns string containing the component's themed CSS variables */ -export const generateComponentThemeCSS = (componentTheme: any, componentName: string): string => { - const cssProps = generateCSSVars(componentTheme, `${CSS_PROPS_PREFIX}${componentName}-`); - - return ` - :host(.${componentName}-themed) { - ${cssProps} +export const generateComponentsThemeCSS = (components: any): string => { + let componentsCSS = ''; + if (components) { + for (const [component, componentTokens] of Object.entries(components)) { + const componentTag = convertToKebabCase(component); + const vars = generateCSSVars(componentTokens, `--${componentTag}-`); + + const componentBlock = ` + ${CSS_ROOT_SELECTOR} ${componentTag} { + ${vars} + } + `; + + // 3. Append the block to the main componentsCSS string + componentsCSS += componentBlock; } + } +console.log('componentsCSS', componentsCSS); + const css = ` + ${componentsCSS} `; + + return css; }; /** @@ -376,34 +383,50 @@ export const generateComponentThemeCSS = (componentTheme: any, componentName: st * @param element The element to apply the theme to * @returns true if theme was applied, false otherwise */ -export const applyComponentTheme = (element: HTMLElement): void => { - const customTheme = (window as any).Ionic?.config?.get?.('customTheme'); +export const applyComponentsTheme = (theme: any): any => { + const themeValidity = checkThemeValidity(theme, 'applyComponentsTheme'); + if (!themeValidity) { + return ''; + } + + // grab all the default components from theme + const { components } = theme; + + // check if there is no components then return + if (!components) { + return ''; + } - // Convert 'ION-CHIP' to 'ion-chip' and split into parts - const parts = element.tagName.toLowerCase().split('-'); + injectCSS(generateComponentsThemeCSS(components)); + return components; - // Get the component name 'chip' from the second part - const componentName = parts[1]; + // const customTheme = (window as any).Ionic?.config?.get?.('customTheme'); - // Convert to 'IonChip' by capitalizing each part - const themeLookupName = parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(''); + // // Convert 'ION-CHIP' to 'ion-chip' and split into parts + // const parts = element.tagName.toLowerCase().split('-'); - // Get the component theme from the global custom theme if it exists - const componentTheme = customTheme?.components?.[themeLookupName]; + // // Get the component name 'chip' from the second part + // const componentName = parts[1]; - if (componentTheme) { - // Add the theme class to the element (e.g., 'chip-themed') - const themeClass = `${componentName}-themed`; - element.classList.add(themeClass); + // // Convert to 'IonChip' by capitalizing each part + // const themeLookupName = parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(''); - // Generate CSS custom properties inside a theme class selector - const css = generateComponentThemeCSS(componentTheme, componentName); + // // Get the component theme from the global custom theme if it exists + // const componentTheme = customTheme?.components?.[themeLookupName]; - // Inject styles into shadow root if available, - // otherwise into the element itself - const root = element.shadowRoot ?? element; - injectCSS(css, root); - } + // if (componentTheme) { + // // Add the theme class to the element (e.g., 'chip-themed') + // const themeClass = `${componentName}-themed`; + // element.classList.add(themeClass); + + // // Generate CSS custom properties inside a theme class selector + // const css = generateComponentsThemeCSS(componentTheme, componentName); + + // // Inject styles into shadow root if available, + // // otherwise into the element itself + // const root = element.shadowRoot ?? element; + // injectCSS(css, root); + // } }; /** @@ -471,3 +494,88 @@ export const mix = (baseColor: string, mixColor: string, weight: string): string const toHex = (n: number) => n.toString(16).padStart(2, '0'); return `#${toHex(r)}${toHex(g)}${toHex(b)}`; }; + +/** + * Converts a string to kebab-case + * + * @param str The string to convert (e.g., 'IonChip') + * @returns The kebab-case string (e.g., 'ion-chip') + */ +const convertToKebabCase = (str: string): string => { + // It's already kebab-case + if (str.indexOf('-') !== -1) { + return str.toLowerCase(); + } + + return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); +}; + +const checkThemeValidity = (theme: any, source: string): boolean => { + if (typeof theme !== 'object' || Array.isArray(theme)) { + console.warn(`${source}: Invalid theme object provided`, theme); + return false; + } + + if (Object.keys(theme).length === 0) { + console.warn(`${source}: Empty theme object provided`); + return false; + } + + return true; +} + +/** + * Mimics the Sass `rgba` function logic to construct CSS rgba() values. + * + * @param colorRgb The RGB color components as a string (e.g., '255, 0, 0'). + * @param alpha The opacity value (0 to 1). + * @returns A string containing the CSS rgba() function call. + */ +export function rgba(colorRgb: string, alpha: number | string): string { + // This directly constructs the rgba() function call using the provided values. + return `rgba(${colorRgb}, ${alpha})`; +} + +/** + * Mimics the Ionic Framework `current-color` function logic to construct CSS color values. + * + * @param variation The color variation (e.g., 'primary', 'secondary', 'base'). + * @param alpha The opacity value (0 to 1). If null, returns the full color variable. + * @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 { + // 1. Determine the base CSS variable name + const variable = subtle + ? `--ion-color-subtle-${variation}` + : `--ion-color-${variation}`; + + // 2. Handle the case where no alpha is provided + 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}); + + // 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})`; + } +} + +/** + * Mimics the CSS `clamp` function logic. + * + * @param min The minimum value + * @param val The preferred value + * @param max The maximum value + * @returns + */ +export function clamp(min: number | string, val: number | string, max: number | string): string { + return `clamp(${min}, ${val}, ${max})`; +} From 0d6bd9dde21098c5c2fd9a54880c290bd4c52c32 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 15 Dec 2025 16:50:51 -0800 Subject: [PATCH 02/13] Update core/src/components/chip/chip.tsx Co-authored-by: Brandy Smith --- core/src/components/chip/chip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/components/chip/chip.tsx b/core/src/components/chip/chip.tsx index edd5d980ea4..9584c8b5367 100644 --- a/core/src/components/chip/chip.tsx +++ b/core/src/components/chip/chip.tsx @@ -64,7 +64,7 @@ export class Chip implements ComponentInterface { /** * Set to `"small"` for a chip with less height and padding. * - * Defaults to `"large"` for the ionic theme, and undefined for all other themes. + * Defaults to `"medium"`. */ @Prop() size?: 'small' | 'medium' | 'large' = 'medium'; From ade5c053284a287f70969462bdfe4e42a54f1cc3 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 15 Dec 2025 16:52:54 -0800 Subject: [PATCH 03/13] Update core/src/utils/theme.ts Co-authored-by: Brandy Smith --- core/src/utils/theme.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/core/src/utils/theme.ts b/core/src/utils/theme.ts index 8c37d4fd044..1a55c20b630 100644 --- a/core/src/utils/theme.ts +++ b/core/src/utils/theme.ts @@ -350,7 +350,29 @@ export const applyGlobalTheme = (baseTheme: any, userTheme?: any): any => { * Generates component's themed CSS class with CSS variables * from its theme object * @param componentTheme The component's object to generate CSS for (e.g., IonChip { }) - * @param componentName The component name without any prefixes (e.g., 'chip') + * @param components An object mapping component names (e.g. `IonChip`) to a nested + * design-token configuration. Each configuration can contain arbitrary levels of + * token groups (such as `size`, `state`, `shape`, `variant`, etc.), where leaf values + * are CSS-compatible values. The structure is recursively flattened into CSS custom + * properties using kebab-case keys and an `--ion--` prefix. + * + * Example: + * ```json + * { + * IonChip: { + * size: { small: { height: "24px" } }, + * state: { disabled: { opacity: "0.4" } } + * } + * } + * ``` + * + * Becomes: + * ```css + * :root ion-chip { + * --ion-chip-size-small-height: 24px; + * --ion-chip-state-disabled-opacity: 0.4; + * } + * ``` * @returns string containing the component's themed CSS variables */ export const generateComponentsThemeCSS = (components: any): string => { From 0349ff7ffc18854791f96dbae6bbdac5837d2253 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 15 Dec 2025 16:54:13 -0800 Subject: [PATCH 04/13] Update core/src/utils/theme.ts Co-authored-by: Brandy Smith --- core/src/utils/theme.ts | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/core/src/utils/theme.ts b/core/src/utils/theme.ts index 1a55c20b630..bbcabb69f65 100644 --- a/core/src/utils/theme.ts +++ b/core/src/utils/theme.ts @@ -376,29 +376,24 @@ export const applyGlobalTheme = (baseTheme: any, userTheme?: any): any => { * @returns string containing the component's themed CSS variables */ export const generateComponentsThemeCSS = (components: any): string => { - let componentsCSS = ''; - if (components) { - for (const [component, componentTokens] of Object.entries(components)) { - const componentTag = convertToKebabCase(component); - const vars = generateCSSVars(componentTokens, `--${componentTag}-`); - - const componentBlock = ` - ${CSS_ROOT_SELECTOR} ${componentTag} { - ${vars} - } - `; - - // 3. Append the block to the main componentsCSS string - componentsCSS += componentBlock; - } + let css = ''; + + for (const [component, componentTokens] of Object.entries(components)) { + const componentTag = convertToKebabCase(component); + const vars = generateCSSVars(componentTokens, `--${componentTag}-`); + + const componentBlock = ` + ${CSS_ROOT_SELECTOR} ${componentTag} { + ${vars} + } + `; + + css += componentBlock; } -console.log('componentsCSS', componentsCSS); - const css = ` - ${componentsCSS} - `; return css; }; +}; /** * Applies a component theme to an element if it exists in the custom theme From ee81a8ac4bc83ac1a72b9d0c5898b2fc1948044d Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 15 Dec 2025 16:56:42 -0800 Subject: [PATCH 05/13] fix(theme): remove extra curly bracket --- core/src/utils/theme.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/core/src/utils/theme.ts b/core/src/utils/theme.ts index bbcabb69f65..bec75b5b9a3 100644 --- a/core/src/utils/theme.ts +++ b/core/src/utils/theme.ts @@ -393,7 +393,6 @@ export const generateComponentsThemeCSS = (components: any): string => { return css; }; -}; /** * Applies a component theme to an element if it exists in the custom theme From c6dfb1043740883ef84d5fe58150b3bb2dd74ba0 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 15 Dec 2025 16:58:57 -0800 Subject: [PATCH 06/13] refactor(theme): remove root --- core/src/utils/theme.ts | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/core/src/utils/theme.ts b/core/src/utils/theme.ts index bec75b5b9a3..310d5a2f161 100644 --- a/core/src/utils/theme.ts +++ b/core/src/utils/theme.ts @@ -383,7 +383,7 @@ export const generateComponentsThemeCSS = (components: any): string => { const vars = generateCSSVars(componentTokens, `--${componentTag}-`); const componentBlock = ` - ${CSS_ROOT_SELECTOR} ${componentTag} { + ${componentTag} { ${vars} } `; @@ -407,7 +407,7 @@ export const applyComponentsTheme = (theme: any): any => { // grab all the default components from theme const { components } = theme; - + // check if there is no components then return if (!components) { return ''; @@ -513,7 +513,7 @@ export const mix = (baseColor: string, mixColor: string, weight: string): string /** * Converts a string to kebab-case - * + * * @param str The string to convert (e.g., 'IonChip') * @returns The kebab-case string (e.g., 'ion-chip') */ @@ -531,14 +531,14 @@ const checkThemeValidity = (theme: any, source: string): boolean => { console.warn(`${source}: Invalid theme object provided`, theme); return false; } - + if (Object.keys(theme).length === 0) { console.warn(`${source}: Empty theme object provided`); return false; } return true; -} +}; /** * Mimics the Sass `rgba` function logic to construct CSS rgba() values. @@ -560,15 +560,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, alpha: number | string | null = null, subtle: boolean = false): string { // 1. Determine the base CSS variable name - const variable = subtle - ? `--ion-color-subtle-${variation}` - : `--ion-color-${variation}`; + const variable = subtle ? `--ion-color-subtle-${variation}` : `--ion-color-${variation}`; // 2. Handle the case where no alpha is provided if (alpha === null) { @@ -586,11 +580,11 @@ export function currentColor( /** * Mimics the CSS `clamp` function logic. - * + * * @param min The minimum value * @param val The preferred value * @param max The maximum value - * @returns + * @returns */ export function clamp(min: number | string, val: number | string, max: number | string): string { return `clamp(${min}, ${val}, ${max})`; From e795c61ba6550f878567a2bcd06d9ed7f24794fa Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 15 Dec 2025 16:59:53 -0800 Subject: [PATCH 07/13] chore(components): run build --- core/src/components.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/components.d.ts b/core/src/components.d.ts index f5ebc5b6328..80be04dd028 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -890,7 +890,7 @@ export namespace Components { */ "shape"?: 'soft' | 'round' | 'rectangular'; /** - * Set to `"small"` for a chip with less height and padding. Defaults to `"large"` for the ionic theme, and undefined for all other themes. + * Set to `"small"` for a chip with less height and padding. Defaults to `"medium"`. * @default 'medium' */ "size"?: 'small' | 'medium' | 'large'; @@ -6861,7 +6861,7 @@ declare namespace LocalJSX { */ "shape"?: 'soft' | 'round' | 'rectangular'; /** - * Set to `"small"` for a chip with less height and padding. Defaults to `"large"` for the ionic theme, and undefined for all other themes. + * Set to `"small"` for a chip with less height and padding. Defaults to `"medium"`. * @default 'medium' */ "size"?: 'small' | 'medium' | 'large'; From 39bfce5d9290c7197d1876851566806debea7e9e Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 15 Dec 2025 17:00:36 -0800 Subject: [PATCH 08/13] chore(many): run lint --- core/src/components/chip/chip.base.scss | 19 +- core/src/components/chip/chip.base.vars.scss | 2 +- core/src/themes/ios/default.tokens.ts | 205 +++++++++---------- core/src/themes/md/default.tokens.ts | 8 +- core/src/themes/mixins.scss | 2 +- core/src/themes/themes.interfaces.ts | 8 +- core/src/utils/test/theme.spec.ts | 20 +- 7 files changed, 137 insertions(+), 127 deletions(-) diff --git a/core/src/components/chip/chip.base.scss b/core/src/components/chip/chip.base.scss index ff9397b509b..6aeb2930583 100644 --- a/core/src/components/chip/chip.base.scss +++ b/core/src/components/chip/chip.base.scss @@ -197,7 +197,11 @@ } ::slotted(ion-icon:first-child) { - @include mixins.margin(vars.$chip-icon-first-child-margin, vars.$chip-icon-first-child-margin-end, $start: vars.$chip-icon-first-child-margin); + @include mixins.margin( + vars.$chip-icon-first-child-margin, + vars.$chip-icon-first-child-margin-end, + $start: vars.$chip-icon-first-child-margin + ); } ::slotted(ion-icon:last-child) { @@ -213,10 +217,17 @@ } ::slotted(ion-avatar:first-child) { - @include mixins.margin(vars.$chip-avatar-first-child-margin-vertical, $end: vars.$chip-avatar-first-child-margin-end, $start: vars.$chip-avatar-first-child-margin-start); + @include mixins.margin( + vars.$chip-avatar-first-child-margin-vertical, + $end: vars.$chip-avatar-first-child-margin-end, + $start: vars.$chip-avatar-first-child-margin-start + ); } ::slotted(ion-avatar:last-child) { - @include mixins.margin(vars.$chip-avatar-last-child-margin-vertical, $end: vars.$chip-avatar-last-child-margin-end, $start: vars.$chip-avatar-last-child-margin-start); + @include mixins.margin( + vars.$chip-avatar-last-child-margin-vertical, + $end: vars.$chip-avatar-last-child-margin-end, + $start: vars.$chip-avatar-last-child-margin-start + ); } - diff --git a/core/src/components/chip/chip.base.vars.scss b/core/src/components/chip/chip.base.vars.scss index 47f578de860..412121e44dc 100644 --- a/core/src/components/chip/chip.base.vars.scss +++ b/core/src/components/chip/chip.base.vars.scss @@ -146,4 +146,4 @@ $chip-avatar-last-child-margin-vertical: var(--ion-chip-avatar-last-child-margin $chip-avatar-last-child-margin-start: var(--ion-chip-avatar-last-child-margin-start); /// @prop - Avatar margin end for last child -$chip-avatar-last-child-margin-end: var(--ion-chip-avatar-last-child-margin-end); \ No newline at end of file +$chip-avatar-last-child-margin-end: var(--ion-chip-avatar-last-child-margin-end); diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts index 268f051d4dd..44352fe4037 100644 --- a/core/src/themes/ios/default.tokens.ts +++ b/core/src/themes/ios/default.tokens.ts @@ -10,11 +10,11 @@ const colors = { backgroundColorRgb: 'var(--ion-background-color-rgb, 255, 255, 255)', textColor: 'var(--ion-text-color, #000)', textColorRgb: 'var(--ion-text-color-rgb, 0, 0, 0)', -} +}; const fontSizes = { chipBase: 14, -} +}; export const defaultTheme: DefaultTheme = { ...baseDefaultTheme, @@ -84,119 +84,118 @@ export const defaultTheme: DefaultTheme = { xxxxl: 'var(--ion-radii-full)', }, - components: { - IonChip: { - margin: '4px', - paddingVertical: '6px', - paddingHorizontal: '12px', - - // Sizes - size: { - small: { - height: '24px', - fontSize: clamp('12px', `${(fontSizes.chipBase - 2) / 16}rem`, '20px'), - }, - medium: { - height: '32px', - fontSize: clamp('13px', `${fontSizes.chipBase / 16}rem`, '22px'), - }, - large: { - height: '32px', - fontSize: clamp('14px', `${(fontSizes.chipBase + 2) / 16}rem`, '24px'), - }, + IonChip: { + margin: '4px', + paddingVertical: '6px', + paddingHorizontal: '12px', + + // Sizes + size: { + small: { + height: '24px', + fontSize: clamp('12px', `${(fontSizes.chipBase - 2) / 16}rem`, '20px'), }, - - // States - state: { - disabled: { - opacity: '0.4', - }, - focus: { - bg: rgba(colors.textColorRgb, 0.16), - semanticBg: currentColor('base', 0.12), - outlineBg: rgba(colors.textColorRgb, 0.04), - }, - activated: { - bg: rgba(colors.textColorRgb, 0.2), - semanticBg: currentColor('base', 0.16), - }, - hover: { - bg: rgba(colors.textColorRgb, 0.16), - semanticBg: currentColor('base', 0.12), - outlineBg: rgba(colors.textColorRgb, 0.04), - }, + medium: { + height: '32px', + fontSize: clamp('13px', `${fontSizes.chipBase / 16}rem`, '22px'), }, - - // Shapes - shape: { - soft: { - borderRadius: 'var(--ion-radii-250)', - }, - round: { - borderRadius: 'var(--ion-radii-full)', - }, - rectangular: { - borderRadius: 'var(--ion-radii-0)', - }, + large: { + height: '32px', + fontSize: clamp('14px', `${(fontSizes.chipBase + 2) / 16}rem`, '24px'), }, - - // Hues - hue: { - bold: { - bg: rgba(colors.textColorRgb, 0.12), - color: rgba(colors.textColorRgb, 0.87), - - outline: { - borderColor: rgba(colors.textColorRgb, 0.32), - }, - - // Any of the semantic colors like primary, secondary, etc. - semantic: { - bgAlpha: '0.08', - color: currentColor('shade'), - - outline: { - borderColor: currentColor('base', 0.32), - } - }, + }, + + // States + state: { + disabled: { + opacity: '0.4', + }, + focus: { + bg: rgba(colors.textColorRgb, 0.16), + semanticBg: currentColor('base', 0.12), + outlineBg: rgba(colors.textColorRgb, 0.04), + }, + activated: { + bg: rgba(colors.textColorRgb, 0.2), + semanticBg: currentColor('base', 0.16), + }, + hover: { + bg: rgba(colors.textColorRgb, 0.16), + semanticBg: currentColor('base', 0.12), + outlineBg: rgba(colors.textColorRgb, 0.04), + }, + }, + + // Shapes + shape: { + soft: { + borderRadius: 'var(--ion-radii-250)', + }, + round: { + borderRadius: 'var(--ion-radii-full)', + }, + rectangular: { + borderRadius: 'var(--ion-radii-0)', + }, + }, + + // Hues + hue: { + bold: { + bg: rgba(colors.textColorRgb, 0.12), + color: rgba(colors.textColorRgb, 0.87), + + outline: { + borderColor: rgba(colors.textColorRgb, 0.32), }, - subtle: { - bg: rgba(colors.textColorRgb, 0.04), - color: rgba(colors.textColorRgb, 0.87), - + + // Any of the semantic colors like primary, secondary, etc. + semantic: { + bgAlpha: '0.08', + color: currentColor('shade'), + outline: { - borderColor: rgba(colors.textColorRgb, 0.32), + borderColor: currentColor('base', 0.32), }, }, }, - - // Variants - variant: { + subtle: { + bg: rgba(colors.textColorRgb, 0.04), + color: rgba(colors.textColorRgb, 0.87), + outline: { - borderWidth: '1px', - bg: 'transparent', + borderColor: rgba(colors.textColorRgb, 0.32), }, }, - - icon: { - size: `${20 / fontSizes.chipBase}em`, - color: rgba(colors.textColorRgb, 0.54), - firstChildMargin: '-4px', - firstChildMarginEnd: '8px', - lastChildMargin: '-4px', - lastChildMarginStart: '8px', - }, - - avatar: { - size: `${24 / fontSizes.chipBase}em`, - firstChildMarginVertical: '-4px', - firstChildMarginStart: '-8px', - firstChildMarginEnd: '8px', - lastChildMarginVertical: '-4px', - lastChildMarginStart: '8px', - lastChildMarginEnd: '-8px', + }, + + // Variants + variant: { + outline: { + borderWidth: '1px', + bg: 'transparent', }, }, - } + + icon: { + size: `${20 / fontSizes.chipBase}em`, + color: rgba(colors.textColorRgb, 0.54), + firstChildMargin: '-4px', + firstChildMarginEnd: '8px', + lastChildMargin: '-4px', + lastChildMarginStart: '8px', + }, + + avatar: { + size: `${24 / fontSizes.chipBase}em`, + firstChildMarginVertical: '-4px', + firstChildMarginStart: '-8px', + firstChildMarginEnd: '8px', + lastChildMarginVertical: '-4px', + lastChildMarginStart: '8px', + lastChildMarginEnd: '-8px', + }, + }, + }, }; diff --git a/core/src/themes/md/default.tokens.ts b/core/src/themes/md/default.tokens.ts index a491edc109f..b6516532ce5 100644 --- a/core/src/themes/md/default.tokens.ts +++ b/core/src/themes/md/default.tokens.ts @@ -10,11 +10,11 @@ const colors = { backgroundColorRgb: 'var(--ion-background-color-rgb, 255, 255, 255)', textColor: 'var(--ion-text-color, #000)', textColorRgb: 'var(--ion-text-color-rgb, 0, 0, 0)', -} +}; const fontSizes = { chipBase: 14, -} +}; export const defaultTheme: DefaultTheme = { ...baseDefaultTheme, @@ -162,7 +162,7 @@ export const defaultTheme: DefaultTheme = { outline: { borderColor: currentColor('base', 0.32), - } + }, }, }, subtle: { @@ -202,5 +202,5 @@ export const defaultTheme: DefaultTheme = { lastChildMarginEnd: '-8px', }, }, - } + }, }; diff --git a/core/src/themes/mixins.scss b/core/src/themes/mixins.scss index c9eba257a8c..10edafd19ef 100644 --- a/core/src/themes/mixins.scss +++ b/core/src/themes/mixins.scss @@ -1,5 +1,5 @@ @use "./functions.string" as string; -@use 'sass:meta'; +@use "sass:meta"; /** * A heuristic that applies CSS to tablet diff --git a/core/src/themes/themes.interfaces.ts b/core/src/themes/themes.interfaces.ts index ab94c3a99a2..9d2349f01f6 100644 --- a/core/src/themes/themes.interfaces.ts +++ b/core/src/themes/themes.interfaces.ts @@ -326,7 +326,7 @@ type Components = { outline: { borderColor: string; - } + }; // Any of the semantic colors like primary, secondary, etc. semantic: { @@ -335,7 +335,7 @@ type Components = { outline: { borderColor: string; - } + }; }; }; subtle: { @@ -344,7 +344,7 @@ type Components = { outline: { borderColor: string; - } + }; }; }; @@ -381,4 +381,4 @@ type Components = { IonTabBar?: any; IonModal?: any; IonToolbar?: any; -}; \ No newline at end of file +}; diff --git a/core/src/utils/test/theme.spec.ts b/core/src/utils/test/theme.spec.ts index 8edb5a4d6ac..0ffd2ee0e52 100644 --- a/core/src/utils/test/theme.spec.ts +++ b/core/src/utils/test/theme.spec.ts @@ -562,18 +562,18 @@ describe('generateComponentsThemeCSS', () => { const components = { IonChip: { hue: { - subtle: { - bg: 'red', - color: 'white', - borderColor: 'black', - }, - bold: { - bg: 'blue', - color: 'white', - borderColor: 'black', + subtle: { + bg: 'red', + color: 'white', + borderColor: 'black', + }, + bold: { + bg: 'blue', + color: 'white', + borderColor: 'black', + }, }, }, - } }; const css = generateComponentsThemeCSS(components).replace(/\s/g, ''); From b67f4f5254bda72a8a1034124cfb34687dd1e1a3 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 15 Dec 2025 17:04:40 -0800 Subject: [PATCH 09/13] test(theme): add more generateComponentsThemeCSS tests --- core/src/utils/test/theme.spec.ts | 69 ++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/core/src/utils/test/theme.spec.ts b/core/src/utils/test/theme.spec.ts index 0ffd2ee0e52..f21ade1fadd 100644 --- a/core/src/utils/test/theme.spec.ts +++ b/core/src/utils/test/theme.spec.ts @@ -558,7 +558,7 @@ describe('generateGlobalThemeCSS', () => { }); describe('generateComponentsThemeCSS', () => { - it('should generate component theme CSS for a given theme', () => { + it('should generate component theme CSS for a given theme with a single component', () => { const components = { IonChip: { hue: { @@ -579,7 +579,7 @@ describe('generateComponentsThemeCSS', () => { const css = generateComponentsThemeCSS(components).replace(/\s/g, ''); const expectedCSS = ` - :root ion-chip { + ion-chip { --ion-chip-hue-subtle-bg: red; --ion-chip-hue-subtle-color: white; --ion-chip-hue-subtle-border-color: black; @@ -591,6 +591,71 @@ describe('generateComponentsThemeCSS', () => { expect(css).toBe(expectedCSS); }); + + it('should generate component theme CSS for a given theme with multiple components', () => { + const components = { + IonChip: { + hue: { + subtle: { + bg: 'red', + color: 'white', + borderColor: 'black', + }, + bold: { + bg: 'blue', + color: 'white', + borderColor: 'black', + }, + }, + }, + IonBadge: { + hue: { + subtle: { + bg: 'green', + color: 'white', + borderColor: 'black', + }, + bold: { + bg: 'blue', + color: 'white', + borderColor: 'black', + }, + }, + }, + }; + + const css = generateComponentsThemeCSS(components).replace(/\s/g, ''); + + const expectedCSS = ` + ion-chip { + --ion-chip-hue-subtle-bg: red; + --ion-chip-hue-subtle-color: white; + --ion-chip-hue-subtle-border-color: black; + --ion-chip-hue-bold-bg: blue; + --ion-chip-hue-bold-color: white; + --ion-chip-hue-bold-border-color: black; + } + + ion-badge { + --ion-badge-hue-subtle-bg: green; + --ion-badge-hue-subtle-color: white; + --ion-badge-hue-subtle-border-color: black; + --ion-badge-hue-bold-bg: blue; + --ion-badge-hue-bold-color: white; + --ion-badge-hue-bold-border-color: black; + } + `.replace(/\s/g, ''); + + expect(css).toBe(expectedCSS); + }); + + it('should not generate CSS variables for an empty components object', () => { + const components = {}; + + const css = generateComponentsThemeCSS(components); + + expect(css).toBe(''); + }); }); describe('generateColorClasses', () => { From 9ecb1032c6fa4b2e0b0873fb87ce86909003a5aa Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 15 Dec 2025 17:11:29 -0800 Subject: [PATCH 10/13] Update core/src/themes/mixins.scss Co-authored-by: Brandy Smith --- core/src/themes/mixins.scss | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/core/src/themes/mixins.scss b/core/src/themes/mixins.scss index 10edafd19ef..1a2afeb58e9 100644 --- a/core/src/themes/mixins.scss +++ b/core/src/themes/mixins.scss @@ -611,12 +611,15 @@ } } -// Focused mixin to be used on regular elements +// Mixin that applies focus styles to interactive elements. // -// ex: :host(.ion-focused) .toggle-icon { -// @include mixins.focused-state(); -// } +// Example: // +// ```scss +// :host(.ion-focused) .toggle-icon { +// @include mixins.focused-state(); +// } +// ``` // -------------------------------------------------- @mixin focused-state($width: null, $style: solid, $color: null, $addOffset: true) { @if $width == null or $color == null { From 7040091786664b90640645323c14b16c259d2746 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 15 Dec 2025 17:13:46 -0800 Subject: [PATCH 11/13] refactor(theme): seperate objects --- core/src/themes/ios/default.tokens.ts | 6 ++++-- core/src/themes/md/default.tokens.ts | 6 ++++-- core/src/themes/themes.interfaces.ts | 8 ++++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts index 44352fe4037..6ee0f5c9d0b 100644 --- a/core/src/themes/ios/default.tokens.ts +++ b/core/src/themes/ios/default.tokens.ts @@ -87,8 +87,10 @@ export const defaultTheme: DefaultTheme = { components: { IonChip: { margin: '4px', - paddingVertical: '6px', - paddingHorizontal: '12px', + padding: { + vertical: '6px', + horizontal: '12px', + }, // Sizes size: { diff --git a/core/src/themes/md/default.tokens.ts b/core/src/themes/md/default.tokens.ts index b6516532ce5..97538172b0b 100644 --- a/core/src/themes/md/default.tokens.ts +++ b/core/src/themes/md/default.tokens.ts @@ -92,8 +92,10 @@ export const defaultTheme: DefaultTheme = { components: { IonChip: { margin: '4px', - paddingVertical: '6px', - paddingHorizontal: '12px', + padding: { + vertical: '6px', + horizontal: '12px', + }, // Sizes size: { diff --git a/core/src/themes/themes.interfaces.ts b/core/src/themes/themes.interfaces.ts index 9d2349f01f6..1a40f097064 100644 --- a/core/src/themes/themes.interfaces.ts +++ b/core/src/themes/themes.interfaces.ts @@ -261,8 +261,12 @@ export type DefaultTheme = BaseTheme & { type Components = { IonChip?: { margin: string | number; - paddingVertical: string | number; - paddingHorizontal: string | number; + + padding?: { + vertical: string | number; + horizontal: string | number; + }; + gap?: string | number; lineHeight?: string | number; From 098cba4096554dee16632e175ca80b94588117db Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Wed, 17 Dec 2025 18:15:04 -0800 Subject: [PATCH 12/13] feat(chip): adding ionic theme --- core/src/components.d.ts | 12 +- core/src/components/chip/chip.base.scss | 27 ++- core/src/components/chip/chip.base.vars.scss | 23 ++- core/src/components/chip/chip.tsx | 4 +- core/src/themes/ionic/default.tokens.ts | 111 +++++++++++++ core/src/themes/ionic/light.tokens.ts | 163 +++++++++++++++++++ core/src/themes/ios/default.tokens.ts | 13 +- core/src/themes/md/default.tokens.ts | 13 +- core/src/themes/mixins.scss | 17 ++ core/src/themes/themes.interfaces.ts | 61 ++++--- core/src/utils/theme.ts | 84 ++++++++++ core/tsconfig.json | 3 +- 12 files changed, 469 insertions(+), 62 deletions(-) diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 80be04dd028..63242ed45e4 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -890,10 +890,10 @@ export namespace Components { */ "shape"?: 'soft' | 'round' | 'rectangular'; /** - * Set to `"small"` for a chip with less height and padding. Defaults to `"medium"`. - * @default 'medium' + * Set to `"small"` for a chip with less height and padding. Defaults to `"small"`. + * @default 'large' */ - "size"?: 'small' | 'medium' | 'large'; + "size"?: 'small' | 'large'; /** * The theme determines the visual appearance of the component. */ @@ -6861,10 +6861,10 @@ declare namespace LocalJSX { */ "shape"?: 'soft' | 'round' | 'rectangular'; /** - * Set to `"small"` for a chip with less height and padding. Defaults to `"medium"`. - * @default 'medium' + * Set to `"small"` for a chip with less height and padding. Defaults to `"small"`. + * @default 'large' */ - "size"?: 'small' | 'medium' | 'large'; + "size"?: 'small' | 'large'; /** * The theme determines the visual appearance of the component. */ diff --git a/core/src/components/chip/chip.base.scss b/core/src/components/chip/chip.base.scss index 6aeb2930583..601fcef814b 100644 --- a/core/src/components/chip/chip.base.scss +++ b/core/src/components/chip/chip.base.scss @@ -1,6 +1,7 @@ @use "../../themes/mixins" as mixins; @use "../../themes/functions.color" as colors; @use "./chip.base.vars.scss" as vars; +@use "sass:meta"; // Chip: Common Styles // -------------------------------------------------- @@ -20,6 +21,7 @@ @include mixins.border-radius(var(--border-radius)); @include mixins.margin(vars.$chip-margin); @include mixins.padding(vars.$chip-padding-vertical, vars.$chip-padding-horizontal); + @include mixins.typography(vars.$chip-typography); display: inline-flex; position: relative; @@ -49,12 +51,6 @@ font-size: vars.$chip-size-small-font-size; } -:host(.chip-medium) { - min-height: vars.$chip-size-medium-height; - - font-size: vars.$chip-size-medium-font-size; -} - :host(.chip-large) { min-height: vars.$chip-size-large-height; @@ -110,6 +106,8 @@ :host(.ion-color.chip-bold.chip-outline) { border-color: vars.$chip-hue-bold-semantic-outline-border-color; + + background: vars.$chip-outline-bold-semantic-bg; //native would be transparent, else it would be whatevers in ion-color.chip-bold } // Subtle @@ -120,6 +118,8 @@ :host(.ion-color.chip-subtle.chip-outline) { border-color: colors.current-color(shade, $subtle: true); + + background: vars.$chip-outline-subtle-semantic-bg; } // Outline Chip @@ -130,11 +130,6 @@ border-style: solid; } -:host(.chip-outline), -:host(.chip-outline.ion-color) { - background: vars.$chip-outline-bg; -} - // Chip States // --------------------------------------------- @@ -210,10 +205,14 @@ // Avatar ::slotted(ion-avatar) { + // width: vars.$chip-avatar-size; + // height: vars.$chip-avatar-size; + // @error vars.$chip-avatar-size; + @if vars.$chip-avatar-size != "unset" { + width: vars.$chip-avatar-size; + height: vars.$chip-avatar-size; + } flex-shrink: 0; - - width: vars.$chip-avatar-size; - height: vars.$chip-avatar-size; } ::slotted(ion-avatar:first-child) { diff --git a/core/src/components/chip/chip.base.vars.scss b/core/src/components/chip/chip.base.vars.scss index 412121e44dc..3251393d08c 100644 --- a/core/src/components/chip/chip.base.vars.scss +++ b/core/src/components/chip/chip.base.vars.scss @@ -76,8 +76,14 @@ $chip-hue-bold-semantic-outline-border-color: var(--ion-chip-hue-bold-semantic-o /// @prop - Outline border width $chip-outline-border-width: var(--ion-chip-variant-outline-border-width); -/// @prop - Outline chip background color -$chip-outline-bg: var(--ion-chip-variant-outline-bg); +/// @prop - Outline bold chip background color for semantic colors +$chip-outline-bold-semantic-bg: var(--ion-chip-hue-bold-semantic-outline-bg); + +/// @prop - Subtle chip background color for semantic colors +$chip-hue-subtle-semantic-bg: var(--ion-chip-hue-subtle-semantic-bg); + +/// @prop - Outline subtle chip background color for semantic colors +$chip-outline-subtle-semantic-bg: var(--ion-chip-hue-subtle-semantic-outline-bg); /// @prop - Focus ring color $chip-focus-ring-color: var(--ion-chip-state-focus-ring-color); @@ -128,7 +134,7 @@ $chip-icon-last-child-margin: var(--ion-chip-icon-last-child-margin); $chip-icon-last-child-margin-start: var(--ion-chip-icon-last-child-margin-start); /// @prop - Avatar size -$chip-avatar-size: var(--ion-chip-avatar-size); +$chip-avatar-size: var(--ion-chip-avatar-size, revert-layer); /// @prop - Avatar margin vertical for first child $chip-avatar-first-child-margin-vertical: var(--ion-chip-avatar-first-child-margin-vertical); @@ -147,3 +153,14 @@ $chip-avatar-last-child-margin-start: var(--ion-chip-avatar-last-child-margin-st /// @prop - Avatar margin end for last child $chip-avatar-last-child-margin-end: var(--ion-chip-avatar-last-child-margin-end); + +/// @prop - Typography styles for the chip +$chip-typography: ( + font-family: var(--ion-chip-typography-font-family), + font-size: var(--ion-chip-typography-font-size), + font-weight: var(--ion-chip-typography-font-weight), + letter-spacing: var(--ion-chip-typography-letter-spacing), + line-height: var(--ion-chip-typography-line-height), + text-decoration: var(--ion-chip-typography-text-decoration), + text-transform: var(--ion-chip-typography-text-transform), +); diff --git a/core/src/components/chip/chip.tsx b/core/src/components/chip/chip.tsx index 9584c8b5367..6c92dfafa0f 100644 --- a/core/src/components/chip/chip.tsx +++ b/core/src/components/chip/chip.tsx @@ -64,9 +64,9 @@ export class Chip implements ComponentInterface { /** * Set to `"small"` for a chip with less height and padding. * - * Defaults to `"medium"`. + * Defaults to `"small"`. */ - @Prop() size?: 'small' | 'medium' | 'large' = 'medium'; + @Prop() size?: 'small' | 'large' = 'large'; render() { const { hue, size } = this; diff --git a/core/src/themes/ionic/default.tokens.ts b/core/src/themes/ionic/default.tokens.ts index debcac4092b..a132208d6fc 100644 --- a/core/src/themes/ionic/default.tokens.ts +++ b/core/src/themes/ionic/default.tokens.ts @@ -1,9 +1,22 @@ +import * as colorTokens from 'outsystems-design-tokens/tokens/color scheme.json'; +import * as primitiveTokens from 'outsystems-design-tokens/tokens/primitives.json'; +import * as lightTokens from 'outsystems-design-tokens/tokens/theme/light.json'; +import * as typographyTokens from 'outsystems-design-tokens/tokens/typography.json'; + +import { currentColor, cachedResolveOsToken } from '../../utils/theme'; import { defaultTheme as baseDefaultTheme } from '../base/default.tokens'; import type { DefaultTheme } from '../themes.interfaces'; import { darkTheme } from './dark.tokens'; import { lightTheme } from './light.tokens'; +const tokenMap = { + colorTokens, + primitiveTokens, + lightTokens, + typographyTokens, +}; + export const defaultTheme: DefaultTheme = { ...baseDefaultTheme, @@ -76,4 +89,102 @@ export const defaultTheme: DefaultTheme = { xxxl: 'var(--ion-radii-1000)', xxxxl: 'var(--ion-radii-full)', }, + + components: { + IonChip: { + margin: '0px', + padding: { + vertical: primitiveTokens.scale['150'].$value, + horizontal: primitiveTokens.scale['200'].$value, + }, + typography: cachedResolveOsToken(typographyTokens.body.sm.medium.$value, tokenMap), + lineHeight: primitiveTokens.font['line-height']['full'].$value, + + // Sizes + size: { + small: { + height: primitiveTokens.scale['600'].$value, + fontSize: primitiveTokens.font['font-size']['300'].$value, + }, + large: { + height: primitiveTokens.scale['800'].$value, + fontSize: primitiveTokens.font['font-size']['350'].$value, + }, + }, + + // States + state: { + disabled: { + opacity: '0.4', + }, + focus: { + ring: { + color: lightTokens.primitives.blue['400'].$value, + width: primitiveTokens.scale['050'].$value, + }, + }, + }, + + // Shapes + shape: { + soft: { + borderRadius: primitiveTokens.scale['100'].$value, + }, + round: { + borderRadius: primitiveTokens.scale['400'].$value, + }, + rectangular: { + borderRadius: primitiveTokens.scale['0'].$value, + }, + }, + + // Hues + hue: { + bold: { + bg: cachedResolveOsToken(colorTokens.bg.neutral.bold.default, tokenMap), + color: lightTokens.primitives.base.white.$value, + + outline: { + borderColor: lightTokens.primitives.neutral['1200'].$value, + }, + + // Any of the semantic colors like primary, secondary, etc. + semantic: { + color: currentColor('contrast'), + + outline: { + borderColor: currentColor('shade'), + bg: currentColor('base'), + }, + }, + }, + subtle: { + bg: cachedResolveOsToken(lightTokens.primitives.neutral['100'], tokenMap), + color: lightTokens.primitives.neutral['800'].$value, + + outline: { + borderColor: lightTokens.primitives.neutral['300'].$value, + }, + + semantic: { + outline: { + borderColor: currentColor('shade', null, true), + bg: currentColor('base', null, true), + }, + }, + }, + }, + + // Variants + variant: { + outline: { + borderWidth: primitiveTokens.scale['025'].$value, + }, + }, + + icon: { + size: primitiveTokens.font['font-size']['400'].$value, + }, + }, + }, }; diff --git a/core/src/themes/ionic/light.tokens.ts b/core/src/themes/ionic/light.tokens.ts index 94c96baeb90..6c63c2410d0 100644 --- a/core/src/themes/ionic/light.tokens.ts +++ b/core/src/themes/ionic/light.tokens.ts @@ -1,9 +1,172 @@ +import * as colorTokens from 'outsystems-design-tokens/tokens/color scheme.json'; +import * as primitiveTokens from 'outsystems-design-tokens/tokens/primitives.json'; +import * as lightTokens from 'outsystems-design-tokens/tokens/theme/light.json'; +import * as typographyTokens from 'outsystems-design-tokens/tokens/typography.json'; + +import { cachedResolveOsToken } from '../../utils/theme'; import type { LightTheme } from '../themes.interfaces'; +const tokenMap = { + colorTokens, + lightTokens, + primitiveTokens, + typographyTokens, +}; +console.log( + 'cachedResolveOsToken(colorTokens.bg.primary.base.default, tokenMap)', + cachedResolveOsToken(colorTokens.bg.primary.base.default, tokenMap) +); export const lightTheme: LightTheme = { backgroundColor: '#ffffff', textColor: '#000000', + color: { + primary: { + bold: { + base: cachedResolveOsToken(colorTokens.bg.primary.base.default, tokenMap), + contrast: cachedResolveOsToken(colorTokens.text.inverse, tokenMap), + foreground: cachedResolveOsToken(colorTokens.text.primary, tokenMap), + shade: cachedResolveOsToken(colorTokens.bg.primary.base.press, tokenMap), + tint: cachedResolveOsToken(colorTokens.semantics.primary['600'], tokenMap), + }, + subtle: { + base: cachedResolveOsToken(colorTokens.bg.primary.subtle.default, tokenMap), + contrast: cachedResolveOsToken(colorTokens.text.primary, tokenMap), + foreground: cachedResolveOsToken(colorTokens.text.primary, tokenMap), + shade: cachedResolveOsToken(colorTokens.bg.primary.subtle.press, tokenMap), + tint: cachedResolveOsToken(colorTokens.semantics.primary['200'], tokenMap), + }, + }, + secondary: { + bold: { + base: cachedResolveOsToken(colorTokens.bg.info.base.default, tokenMap), + contrast: cachedResolveOsToken(colorTokens.text.inverse, tokenMap), + foreground: cachedResolveOsToken(colorTokens.text.primary, tokenMap), + shade: cachedResolveOsToken(colorTokens.bg.info.base.press, tokenMap), + tint: cachedResolveOsToken(colorTokens.semantics.info['700'], tokenMap), + }, + subtle: { + base: cachedResolveOsToken(colorTokens.bg.info.subtle.default, tokenMap), + contrast: cachedResolveOsToken(colorTokens.text.info, tokenMap), + foreground: cachedResolveOsToken(colorTokens.text.info, tokenMap), + shade: cachedResolveOsToken(colorTokens.bg.info.subtle.press, tokenMap), + tint: cachedResolveOsToken(colorTokens.semantics.info['200'], tokenMap), + }, + }, + tertiary: { + bold: { + base: cachedResolveOsToken(lightTokens.primitives.violet['700'], tokenMap), + contrast: cachedResolveOsToken(colorTokens.text.inverse, tokenMap), + foreground: cachedResolveOsToken(lightTokens.primitives.violet['700'], tokenMap), + shade: cachedResolveOsToken(lightTokens.primitives.violet['800'], tokenMap), + tint: cachedResolveOsToken(colorTokens.semantics.primary['600'], tokenMap), + }, + subtle: { + base: cachedResolveOsToken(lightTokens.primitives.violet['100'], tokenMap), + contrast: cachedResolveOsToken(lightTokens.primitives.violet['700'], tokenMap), + foreground: cachedResolveOsToken(lightTokens.primitives.violet['700'], tokenMap), + shade: cachedResolveOsToken(lightTokens.primitives.violet['300'], tokenMap), + tint: cachedResolveOsToken(lightTokens.primitives.violet['200'], tokenMap), + }, + }, + success: { + bold: { + base: cachedResolveOsToken(colorTokens.bg.success.base.default, tokenMap), + contrast: cachedResolveOsToken(colorTokens.text.inverse, tokenMap), + foreground: cachedResolveOsToken(colorTokens.text.success, tokenMap), + shade: cachedResolveOsToken(colorTokens.bg.success.base.press, tokenMap), + tint: cachedResolveOsToken(colorTokens.semantics.success['800'], tokenMap), + }, + subtle: { + base: cachedResolveOsToken(colorTokens.bg.success.subtle.default, tokenMap), + contrast: cachedResolveOsToken(colorTokens.text.success, tokenMap), + foreground: cachedResolveOsToken(colorTokens.text.success, tokenMap), + shade: cachedResolveOsToken(colorTokens.bg.success.subtle.press, tokenMap), + tint: cachedResolveOsToken(colorTokens.semantics.success['200'], tokenMap), + }, + }, + warning: { + bold: { + base: cachedResolveOsToken(colorTokens.bg.warning.base.default, tokenMap), + contrast: cachedResolveOsToken(colorTokens.text.inverse, tokenMap), + foreground: cachedResolveOsToken(colorTokens.text.warning, tokenMap), + shade: cachedResolveOsToken(colorTokens.bg.warning.base.press, tokenMap), + tint: cachedResolveOsToken(colorTokens.semantics.warning['800'], tokenMap), + }, + subtle: { + base: cachedResolveOsToken(colorTokens.bg.warning.subtle.default, tokenMap), + contrast: cachedResolveOsToken(colorTokens.text.warning, tokenMap), + foreground: cachedResolveOsToken(colorTokens.text.warning, tokenMap), + shade: cachedResolveOsToken(colorTokens.bg.warning.subtle.press, tokenMap), + tint: cachedResolveOsToken(colorTokens.semantics.warning['200'], tokenMap), + }, + }, + danger: { + bold: { + base: cachedResolveOsToken(colorTokens.bg.danger.base.default, tokenMap), + contrast: cachedResolveOsToken(colorTokens.text.inverse, tokenMap), + foreground: cachedResolveOsToken(colorTokens.text.danger, tokenMap), + shade: cachedResolveOsToken(colorTokens.bg.danger.base.press, tokenMap), + tint: cachedResolveOsToken(colorTokens.semantics.danger['700'], tokenMap), + }, + subtle: { + base: cachedResolveOsToken(colorTokens.bg.danger.subtle.default, tokenMap), + contrast: cachedResolveOsToken(colorTokens.text.danger, tokenMap), + foreground: cachedResolveOsToken(colorTokens.text.danger, tokenMap), + shade: cachedResolveOsToken(colorTokens.bg.danger.subtle.press, tokenMap), + tint: cachedResolveOsToken(colorTokens.semantics.danger['200'], tokenMap), + }, + }, + light: { + bold: { + base: cachedResolveOsToken(lightTokens.primitives.base.white, tokenMap), + contrast: cachedResolveOsToken(colorTokens.text.default, tokenMap), + foreground: cachedResolveOsToken(colorTokens.text.default, tokenMap), + shade: cachedResolveOsToken(lightTokens.primitives.neutral['600'], tokenMap), + tint: cachedResolveOsToken(lightTokens.primitives.neutral['400'], tokenMap), + }, + subtle: { + base: cachedResolveOsToken(colorTokens.bg.neutral.subtlest.default, tokenMap), + contrast: cachedResolveOsToken(colorTokens.text.default, tokenMap), + foreground: cachedResolveOsToken(colorTokens.text.default, tokenMap), + shade: cachedResolveOsToken(colorTokens.bg.neutral.subtlest.press, tokenMap), + tint: cachedResolveOsToken(lightTokens.primitives.neutral['100'], tokenMap), + }, + }, + medium: { + bold: { + base: cachedResolveOsToken(colorTokens.bg.neutral.bold.default, tokenMap), + contrast: cachedResolveOsToken(colorTokens.text.inverse, tokenMap), + foreground: cachedResolveOsToken(colorTokens.text.default, tokenMap), + shade: cachedResolveOsToken(colorTokens.bg.neutral.bold.press, tokenMap), + tint: cachedResolveOsToken(lightTokens.primitives.neutral['900'], tokenMap), + }, + subtle: { + base: cachedResolveOsToken(colorTokens.bg.neutral.subtle.default, tokenMap), + contrast: cachedResolveOsToken(colorTokens.text.subtlest, tokenMap), + foreground: cachedResolveOsToken(colorTokens.text.default, tokenMap), + shade: cachedResolveOsToken(colorTokens.bg.neutral.subtle.press, tokenMap), + tint: cachedResolveOsToken(lightTokens.primitives.neutral['100'], tokenMap), + }, + }, + dark: { + bold: { + base: cachedResolveOsToken(colorTokens.bg.neutral.boldest.default, tokenMap), + contrast: cachedResolveOsToken(colorTokens.text.inverse, tokenMap), + foreground: cachedResolveOsToken(colorTokens.text.default, tokenMap), + shade: cachedResolveOsToken(colorTokens.bg.neutral.boldest.press, tokenMap), + tint: cachedResolveOsToken(lightTokens.primitives.neutral['1100'], tokenMap), + }, + subtle: { + base: cachedResolveOsToken(colorTokens.bg.neutral.subtle.default, tokenMap), + contrast: cachedResolveOsToken(colorTokens.text.subtle, tokenMap), + foreground: cachedResolveOsToken(colorTokens.text.default, tokenMap), + shade: cachedResolveOsToken(colorTokens.bg.neutral.subtle.press, tokenMap), + tint: cachedResolveOsToken(lightTokens.primitives.neutral['100'], tokenMap), + }, + }, + }, + components: { IonCard: { background: '#ffffff', diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts index 6ee0f5c9d0b..b46a7a910c7 100644 --- a/core/src/themes/ios/default.tokens.ts +++ b/core/src/themes/ios/default.tokens.ts @@ -98,10 +98,6 @@ export const defaultTheme: DefaultTheme = { height: '24px', fontSize: clamp('12px', `${(fontSizes.chipBase - 2) / 16}rem`, '20px'), }, - medium: { - height: '32px', - fontSize: clamp('13px', `${fontSizes.chipBase / 16}rem`, '22px'), - }, large: { height: '32px', fontSize: clamp('14px', `${(fontSizes.chipBase + 2) / 16}rem`, '24px'), @@ -159,6 +155,7 @@ export const defaultTheme: DefaultTheme = { outline: { borderColor: currentColor('base', 0.32), + bg: 'transparent', }, }, }, @@ -169,6 +166,13 @@ export const defaultTheme: DefaultTheme = { outline: { borderColor: rgba(colors.textColorRgb, 0.32), }, + + semantic: { + outline: { + borderColor: currentColor('shade'), + bg: 'transparent', + }, + }, }, }, @@ -176,7 +180,6 @@ export const defaultTheme: DefaultTheme = { variant: { outline: { borderWidth: '1px', - bg: 'transparent', }, }, diff --git a/core/src/themes/md/default.tokens.ts b/core/src/themes/md/default.tokens.ts index 97538172b0b..bcd6e5d8a43 100644 --- a/core/src/themes/md/default.tokens.ts +++ b/core/src/themes/md/default.tokens.ts @@ -103,10 +103,6 @@ export const defaultTheme: DefaultTheme = { height: '24px', fontSize: `${(fontSizes.chipBase - 2) / 16}rem`, }, - medium: { - height: '32px', - fontSize: `${fontSizes.chipBase / 16}rem`, - }, large: { height: '32px', fontSize: `${(fontSizes.chipBase + 2) / 16}rem`, @@ -164,6 +160,7 @@ export const defaultTheme: DefaultTheme = { outline: { borderColor: currentColor('base', 0.32), + bg: 'transparent', }, }, }, @@ -174,6 +171,13 @@ export const defaultTheme: DefaultTheme = { outline: { borderColor: rgba(colors.textColorRgb, 0.32), }, + + semantic: { + outline: { + borderColor: currentColor('shade'), + bg: 'transparent', + }, + }, }, }, @@ -181,7 +185,6 @@ export const defaultTheme: DefaultTheme = { variant: { outline: { borderWidth: '1px', - bg: 'transparent', }, }, diff --git a/core/src/themes/mixins.scss b/core/src/themes/mixins.scss index 1a2afeb58e9..85fdfabd4e6 100644 --- a/core/src/themes/mixins.scss +++ b/core/src/themes/mixins.scss @@ -632,3 +632,20 @@ } } } + +// Typography mixin to be used with typography scss variables (ionic.vars.scss) +// +// ex: @include typography($ion-heading-h3-medium); +// +// -------------------------------------------------- +@mixin typography($properties) { + font-family: map-get($properties, font-family); + font-size: map-get($properties, font-size); + font-weight: map-get($properties, font-weight); + + letter-spacing: map-get($properties, letter-spacing); + line-height: map-get($properties, line-height); + + text-decoration: map-get($properties, text-decoration); + text-transform: map-get($properties, text-transform); +} diff --git a/core/src/themes/themes.interfaces.ts b/core/src/themes/themes.interfaces.ts index 1a40f097064..3b785010d82 100644 --- a/core/src/themes/themes.interfaces.ts +++ b/core/src/themes/themes.interfaces.ts @@ -269,6 +269,9 @@ type Components = { gap?: string | number; lineHeight?: string | number; + typography?: { + [key: string]: string | number; + }; // Sizes size: { @@ -276,10 +279,6 @@ type Components = { height: string | number; fontSize: string | number; }; - medium: { - height: string | number; - fontSize?: string | number; - }; large: { height: string | number; fontSize: string | number; @@ -292,17 +291,19 @@ type Components = { opacity: string | number; }; focus: { - ringColor?: string; - ringWidth?: string | number; - bg: string; - semanticBg: string; - outlineBg: string; + ring?: { + color: string; + width?: string | number; + }; + bg?: string; + semanticBg?: string; + outlineBg?: string; }; - activated: { + activated?: { bg: string; semanticBg: string; }; - hover: { + hover?: { bg: string; semanticBg: string; outlineBg: string; @@ -334,11 +335,12 @@ type Components = { // Any of the semantic colors like primary, secondary, etc. semantic: { - bgAlpha: string; + bgAlpha?: string; color: string; outline: { borderColor: string; + bg?: string; }; }; }; @@ -348,6 +350,14 @@ type Components = { outline: { borderColor: string; + bg?: string; + }; + + semantic: { + outline: { + borderColor: string; + bg?: string; + }; }; }; }; @@ -356,27 +366,26 @@ type Components = { variant: { outline: { borderWidth: string | number; - bg: string; }; }; icon: { size: string | number; - color: string; - firstChildMargin: string | number; - firstChildMarginEnd: string | number; - lastChildMargin: string | number; - lastChildMarginStart: string | number; + color?: string; + firstChildMargin?: string | number; + firstChildMarginEnd?: string | number; + lastChildMargin?: string | number; + lastChildMarginStart?: string | number; }; - avatar: { - size: string | number; - firstChildMarginVertical: string | number; - firstChildMarginStart: string | number; - firstChildMarginEnd: string | number; - lastChildMarginVertical: string | number; - lastChildMarginStart: string | number; - lastChildMarginEnd: string | number; + avatar?: { + size: string | number | null; + firstChildMarginVertical?: string | number; + firstChildMarginStart?: string | number; + firstChildMarginEnd?: string | number; + lastChildMarginVertical?: string | number; + lastChildMarginStart?: string | number; + lastChildMarginEnd?: string | number; }; }; diff --git a/core/src/utils/theme.ts b/core/src/utils/theme.ts index 310d5a2f161..e2247169da4 100644 --- a/core/src/utils/theme.ts +++ b/core/src/utils/theme.ts @@ -589,3 +589,87 @@ export function currentColor(variation: string, alpha: number | string | null = export function clamp(min: number | string, val: number | string, max: number | string): string { return `clamp(${min}, ${val}, ${max})`; } + +// Create a cache to store results +const cache = new Map(); + +export const cachedResolveOsToken = (tokenPath: any, tokenMap: Record): any => { + // Use the path/object as the key + // (Note: For objects, this caches by reference) + if (cache.has(tokenPath)) { + return cache.get(tokenPath); + } + + // Use your existing resolveOsToken function with the global tokenMap + const result = resolveOsToken(tokenPath, tokenMap); + + cache.set(tokenPath, result); + return result; +}; + +export const resolveOsToken = (tokenPath: any, tokenMap: Record): any => { + // 1. Handle Objects (like Typography maps) + if (typeof tokenPath === 'object' && tokenPath !== null) { + // If it's a leaf-node token object, unwrap the $value immediately + if ('$value' in tokenPath) { + return resolveOsToken(tokenPath.$value, tokenMap); + } + + // Otherwise, it's a map of multiple tokens, resolve each key + const resolvedObject: Record = {}; + for (const [key, val] of Object.entries(tokenPath)) { + resolvedObject[key] = resolveOsToken(val, tokenMap); + } + return resolvedObject; + } + + // 2. Handle Reference Strings: "{category.path.item}" + let lookupPath = tokenPath; + let isPath = false; + + if (typeof tokenPath === 'string' && tokenPath.startsWith('{') && tokenPath.endsWith('}')) { + const reference = tokenPath.slice(1, -1).trim(); + const [refCategory, ...refPath] = reference.split('.'); + + let rootKey: string | null = null; + switch (refCategory) { + case 'semantics': + rootKey = 'colorTokens'; + break; + case 'font': + rootKey = 'primitiveTokens'; + break; + case 'primitives': + rootKey = 'lightTokens'; + break; + case 'typography': + rootKey = 'typographyTokens'; + break; + case 'scale': + rootKey = 'primitiveTokens'; + break; // Added 'scale' based on your example + } + + if (!rootKey) return tokenPath; + + // As requested, keeping refCategory in the path + lookupPath = `${rootKey}.${refCategory}.${refPath.join('.')}`; + isPath = true; + } + + // 3. ONLY run the reduce if we have confirmed this is a path to be searched + if (isPath) { + const value = lookupPath.split('.').reduce((acc: any, key: string) => { + if (acc && typeof acc === 'object' && key in acc) { + return acc[key]; + } + return undefined; + }, tokenMap); + + // Recursively resolve the result of the lookup + return resolveOsToken(value, tokenMap); + } + + // 4. If it's not a path or a reference, it's a Literal Value (Hex, Font-stack, etc.) + return tokenPath; +}; diff --git a/core/tsconfig.json b/core/tsconfig.json index 6e2271dfde6..5cefdae6512 100644 --- a/core/tsconfig.json +++ b/core/tsconfig.json @@ -34,7 +34,8 @@ "@utils/*": ["src/utils/*"], "@utils/test": ["src/utils/test/utils"], "@global/*": ["src/global/*"] - } + }, + "resolveJsonModule": true, }, "include": [ "src", From 55b610ac4d70c249f865624b06af761331135929 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 9 Jan 2026 13:12:53 -0800 Subject: [PATCH 13/13] feat(themes): add ionic --- core/src/components/chip/chip.base.scss | 48 ++++++++++++++++---- core/src/components/chip/chip.base.vars.scss | 25 ++++++++-- core/src/themes/ionic/default.tokens.ts | 1 + core/src/themes/ios/default.tokens.ts | 22 +-------- core/src/themes/md/default.tokens.ts | 22 +++++++-- core/src/themes/themes.interfaces.ts | 13 ++++++ 6 files changed, 92 insertions(+), 39 deletions(-) diff --git a/core/src/components/chip/chip.base.scss b/core/src/components/chip/chip.base.scss index 601fcef814b..b191effc51b 100644 --- a/core/src/components/chip/chip.base.scss +++ b/core/src/components/chip/chip.base.scss @@ -83,6 +83,7 @@ :host(.chip-bold.chip-outline) { border-color: #{vars.$chip-hue-bold-outline-border-color}; + // background: #{vars.$chip-hue-bold-outline-bg}; } // Subtle @@ -93,6 +94,7 @@ :host(.chip-subtle.chip-outline) { border-color: #{vars.$chip-hue-subtle-outline-border-color}; + // background: #{vars.$chip-hue-subtle-outline-bg}; } // Chip Colors @@ -100,26 +102,26 @@ // Bold :host(.ion-color.chip-bold) { - background: colors.current-color(base, vars.$chip-hue-bold-semantic-bg-alpha); + background: vars.$chip-hue-bold-semantic-bg; color: vars.$chip-hue-bold-semantic-color; } :host(.ion-color.chip-bold.chip-outline) { border-color: vars.$chip-hue-bold-semantic-outline-border-color; - background: vars.$chip-outline-bold-semantic-bg; //native would be transparent, else it would be whatevers in ion-color.chip-bold + // background: vars.$chip-outline-bold-semantic-bg; //native would be transparent, else it would be whatevers in ion-color.chip-bold } // Subtle :host(.ion-color.chip-subtle) { - background: colors.current-color(base, $subtle: true); + background: vars.$chip-hue-subtle-semantic-bg; color: colors.current-color(contrast, $subtle: true); } :host(.ion-color.chip-subtle.chip-outline) { border-color: colors.current-color(shade, $subtle: true); - background: vars.$chip-outline-subtle-semantic-bg; + // background: vars.$chip-outline-subtle-semantic-bg; } // Outline Chip @@ -128,6 +130,16 @@ :host(.chip-outline) { border-width: vars.$chip-outline-border-width; border-style: solid; + + background: var(--ion-chip-variant-outline-bg, var(--background)); +} + +:host(.ion-color.chip-outline.chip-bold) { + background: vars.$chip-outline-bold-semantic-bg; +} + +:host(.ion-color.chip-outline.chip-subtle) { + background: vars.$chip-outline-subtle-semantic-bg; } // Chip States @@ -142,17 +154,33 @@ // Focus :host(.ion-focused) { - --background: #{vars.$chip-focus-bg}; - @include mixins.focused-state(var(--focus-ring-width), $color: var(--focus-ring-color)); } -:host(.ion-focused.ion-color) { - background: vars.$chip-focus-semantic-bg; +:host(.ion-focused.chip-bold) { + --background: var(--ion-chip-state-focus-bg, var(--ion-chip-hue-bold-bg)); +} + +:host(.ion-focused.chip-subtle) { + --background: var(--ion-chip-state-focus-bg, var(--ion-chip-hue-subtle-bg)); +} + +:host(.ion-focused.ion-color.chip-bold) { + background: var(--ion-chip-hue-bold-semantic-state-focus-bg, vars.$chip-hue-bold-semantic-bg); +} + +:host(.ion-focused.ion-color.chip-subtle) { + background: var(--ion-chip-hue-subtle-semantic-state-focus-bg, vars.$chip-hue-subtle-semantic-bg); +} + +:host(.ion-focused.chip-outline.chip-bold:not(.ion-color)) { + // background: vars.$chip-outline-focus-bg; + background: var(--ion-chip-state-focus-outline-bg, var(--ion-chip-hue-bold-bg)); } -:host(.ion-focused.chip-outline:not(.ion-color)) { - background: vars.$chip-outline-focus-bg; +:host(.ion-focused.chip-outline.chip-subtle:not(.ion-color)) { + // background: vars.$chip-outline-focus-bg; + background: var(--ion-chip-state-focus-outline-bg, var(--ion-chip-hue-subtle-bg)); } // Activated diff --git a/core/src/components/chip/chip.base.vars.scss b/core/src/components/chip/chip.base.vars.scss index 3251393d08c..93413c80140 100644 --- a/core/src/components/chip/chip.base.vars.scss +++ b/core/src/components/chip/chip.base.vars.scss @@ -1,3 +1,5 @@ +@use "../../themes/functions.color" as colors; + /// Chip Variables /// --------------------------------------------- @@ -52,9 +54,15 @@ $chip-hue-subtle-bg: var(--ion-chip-hue-subtle-bg); /// @prop - Subtle chip color $chip-hue-subtle-color: var(--ion-chip-hue-subtle-color); +/// @prop - Subtle chip background color for semantic colors +$chip-hue-subtle-semantic-bg: colors.current-color(base, $subtle: true); + /// @prop - Outline subtle chip border color $chip-hue-subtle-outline-border-color: var(--ion-chip-hue-subtle-outline-border-color); +/// @prop - Outline subtle chip background color +$chip-hue-subtle-outline-bg: var(--ion-chip-hue-subtle-outline-bg); + /// @prop - Bold chip background color $chip-hue-bold-bg: var(--ion-chip-hue-bold-bg); @@ -64,8 +72,14 @@ $chip-hue-bold-color: var(--ion-chip-hue-bold-color); /// @prop - Outline bold chip border color $chip-hue-bold-outline-border-color: var(--ion-chip-hue-bold-outline-border-color); +/// @prop - Outline bold chip background color +$chip-hue-bold-outline-bg: var(--ion-chip-hue-bold-outline-bg); + /// @prop - Bold chip background alpha of semantic colors -$chip-hue-bold-semantic-bg-alpha: var(--ion-chip-hue-bold-semantic-bg-alpha); // only native uses this +$chip-hue-bold-semantic-bg-alpha: var(--ion-chip-hue-bold-semantic-bg-alpha, 1); // only native uses this + +/// @prop - Bold chip background color for semantic colors +$chip-hue-bold-semantic-bg: colors.current-color(base, $chip-hue-bold-semantic-bg-alpha); /// @prop - Bold chip color for semantic colors $chip-hue-bold-semantic-color: var(--ion-chip-hue-bold-semantic-color); @@ -76,11 +90,14 @@ $chip-hue-bold-semantic-outline-border-color: var(--ion-chip-hue-bold-semantic-o /// @prop - Outline border width $chip-outline-border-width: var(--ion-chip-variant-outline-border-width); +/// @prop - Outline background color +$chip-outline-bg: var(--ion-chip-variant-outline-bg); + /// @prop - Outline bold chip background color for semantic colors -$chip-outline-bold-semantic-bg: var(--ion-chip-hue-bold-semantic-outline-bg); +$chip-outline-bold-semantic-bg: var($chip-outline-bg, $chip-hue-bold-semantic-bg); -/// @prop - Subtle chip background color for semantic colors -$chip-hue-subtle-semantic-bg: var(--ion-chip-hue-subtle-semantic-bg); +/// @prop - Outline subtle chip background color for semantic colors +$chip-outline-subtle-semantic-bg: var($chip-outline-bg, $chip-hue-subtle-semantic-bg); /// @prop - Outline subtle chip background color for semantic colors $chip-outline-subtle-semantic-bg: var(--ion-chip-hue-subtle-semantic-outline-bg); diff --git a/core/src/themes/ionic/default.tokens.ts b/core/src/themes/ionic/default.tokens.ts index a132208d6fc..4d457a76203 100644 --- a/core/src/themes/ionic/default.tokens.ts +++ b/core/src/themes/ionic/default.tokens.ts @@ -99,6 +99,7 @@ export const defaultTheme: DefaultTheme = { }, typography: cachedResolveOsToken(typographyTokens.body.sm.medium.$value, tokenMap), lineHeight: primitiveTokens.font['line-height']['full'].$value, + gap: cachedResolveOsToken(primitiveTokens.space['100'].$value, tokenMap), // Sizes size: { diff --git a/core/src/themes/ios/default.tokens.ts b/core/src/themes/ios/default.tokens.ts index b46a7a910c7..55930d35c3d 100644 --- a/core/src/themes/ios/default.tokens.ts +++ b/core/src/themes/ios/default.tokens.ts @@ -111,7 +111,6 @@ export const defaultTheme: DefaultTheme = { }, focus: { bg: rgba(colors.textColorRgb, 0.16), - semanticBg: currentColor('base', 0.12), outlineBg: rgba(colors.textColorRgb, 0.04), }, activated: { @@ -144,35 +143,15 @@ export const defaultTheme: DefaultTheme = { bg: rgba(colors.textColorRgb, 0.12), color: rgba(colors.textColorRgb, 0.87), - outline: { - borderColor: rgba(colors.textColorRgb, 0.32), - }, - // Any of the semantic colors like primary, secondary, etc. semantic: { bgAlpha: '0.08', color: currentColor('shade'), - - outline: { - borderColor: currentColor('base', 0.32), - bg: 'transparent', - }, }, }, subtle: { bg: rgba(colors.textColorRgb, 0.04), color: rgba(colors.textColorRgb, 0.87), - - outline: { - borderColor: rgba(colors.textColorRgb, 0.32), - }, - - semantic: { - outline: { - borderColor: currentColor('shade'), - bg: 'transparent', - }, - }, }, }, @@ -180,6 +159,7 @@ export const defaultTheme: DefaultTheme = { variant: { outline: { borderWidth: '1px', + bg: 'transparent', }, }, diff --git a/core/src/themes/md/default.tokens.ts b/core/src/themes/md/default.tokens.ts index bcd6e5d8a43..29a8a039378 100644 --- a/core/src/themes/md/default.tokens.ts +++ b/core/src/themes/md/default.tokens.ts @@ -105,7 +105,7 @@ export const defaultTheme: DefaultTheme = { }, large: { height: '32px', - fontSize: `${(fontSizes.chipBase + 2) / 16}rem`, + fontSize: `${fontSizes.chipBase / 16}rem`, }, }, @@ -116,7 +116,6 @@ export const defaultTheme: DefaultTheme = { }, focus: { bg: rgba(colors.textColorRgb, 0.16), - semanticBg: currentColor('base', 0.12), outlineBg: rgba(colors.textColorRgb, 0.04), }, activated: { @@ -151,6 +150,7 @@ export const defaultTheme: DefaultTheme = { outline: { borderColor: rgba(colors.textColorRgb, 0.32), + // bg: 'transparent', }, // Any of the semantic colors like primary, secondary, etc. @@ -160,7 +160,13 @@ export const defaultTheme: DefaultTheme = { outline: { borderColor: currentColor('base', 0.32), - bg: 'transparent', + // bg: 'transparent', + }, + + state: { + focus: { + bg: currentColor('base', 0.12), + }, }, }, }, @@ -170,12 +176,19 @@ export const defaultTheme: DefaultTheme = { outline: { borderColor: rgba(colors.textColorRgb, 0.32), + // bg: 'transparent', }, semantic: { outline: { borderColor: currentColor('shade'), - bg: 'transparent', + // bg: 'transparent', + }, + + state: { + focus: { + bg: currentColor('base', 0.12, true), + }, }, }, }, @@ -185,6 +198,7 @@ export const defaultTheme: DefaultTheme = { variant: { outline: { borderWidth: '1px', + bg: 'transparent', }, }, diff --git a/core/src/themes/themes.interfaces.ts b/core/src/themes/themes.interfaces.ts index 3b785010d82..8eafa6c85e7 100644 --- a/core/src/themes/themes.interfaces.ts +++ b/core/src/themes/themes.interfaces.ts @@ -342,6 +342,12 @@ type Components = { borderColor: string; bg?: string; }; + + state?: { + focus?: { + bg?: string; + }; + }; }; }; subtle: { @@ -358,6 +364,12 @@ type Components = { borderColor: string; bg?: string; }; + + state?: { + focus?: { + bg?: string; + }; + }; }; }; }; @@ -366,6 +378,7 @@ type Components = { variant: { outline: { borderWidth: string | number; + bg?: string; }; };