diff --git a/.changeset/slim-down-solid-element.md b/.changeset/slim-down-solid-element.md new file mode 100644 index 0000000000..21f572de00 --- /dev/null +++ b/.changeset/slim-down-solid-element.md @@ -0,0 +1,21 @@ +--- +"@solid-design-system/components": minor +--- + +perf: slim down SolidElement base class to reduce bundle size + +Removed rarely-used features from the `SolidElement` base class (inherited by all 59 components) and moved them to opt-in modules, significantly reducing the bundle size for consumers who import individual components. + +**What changed:** + +- Removed `dir` and `lang` reactive properties (unused by any component; native HTML attributes still work) +- Removed `token()` method and `animate.ts` import — moved to standalone `token()` utility in `src/internal/token.ts` (used by 3 components: tag, video, audio) +- Removed `onThemeChange` lifecycle hook — moved to self-managed listeners in the 2 consuming components (icon, audio) +- Removed CSS `@import` for `interactive.css`, `headline.css`, `paragraph.css` — moved to importable modules in `src/internal/shared-styles.ts` (only added by the ~14 components that actually use them) +- Updated Storybook Vite config to process the new `shared-styles.ts` file with Tailwind + +**What remains in SolidElement:** + +- Tailwind `--tw-*` CSS variable resets (needed by all components) +- `box-sizing` reset and `[hidden]` rule +- `emit()` method (used by 20+ components) diff --git a/packages/components/src/components/audio/audio.ts b/packages/components/src/components/audio/audio.ts index d7c58d813d..9006ec7c56 100644 --- a/packages/components/src/components/audio/audio.ts +++ b/packages/components/src/components/audio/audio.ts @@ -8,6 +8,8 @@ import { LocalizeController } from '../../utilities/localize'; import { property, query, state } from 'lit/decorators.js'; import { Wave } from './wave'; import cx from 'classix'; +import { interactiveStyles } from '../../internal/shared-styles'; +import { token } from '../../internal/token'; import SolidElement from '../../internal/solid-element'; import type SdDrawer from '../drawer/drawer'; import type SdRange from '../range/range'; @@ -50,6 +52,18 @@ import type SdRange from '../range/range'; export default class SdAudio extends SolidElement { private readonly localize = new LocalizeController(this); + private readonly _boundThemeChange = () => this.onThemeChange(); + + connectedCallback(): void { + super.connectedCallback(); + this.renderRoot.addEventListener('sd-theme-change', this._boundThemeChange); + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + this.renderRoot.removeEventListener('sd-theme-change', this._boundThemeChange); + } + private readonly hasSlotController = new HasSlotController(this, 'transcript'); /** Reverses the order of the audio controls and timestamps */ @@ -314,9 +328,9 @@ export default class SdAudio extends SolidElement { return null; } - onThemeChange(): void { + private onThemeChange(): void { this.clear(); - setTimeout(this.initAnimation.bind(this), this.token('--sd-duration-fast', 150)); + setTimeout(this.initAnimation.bind(this), token(this, '--sd-duration-fast', 150)); } private initAnimation() { @@ -540,6 +554,7 @@ export default class SdAudio extends SolidElement { static styles = [ ...SolidElement.styles, + interactiveStyles, css` sd-button::part(base) { @apply rounded-full h-12 w-12 flex items-center justify-center; diff --git a/packages/components/src/components/breadcrumb/breadcrumb.ts b/packages/components/src/components/breadcrumb/breadcrumb.ts index 458043efbd..5d8c2a7588 100644 --- a/packages/components/src/components/breadcrumb/breadcrumb.ts +++ b/packages/components/src/components/breadcrumb/breadcrumb.ts @@ -5,6 +5,7 @@ import { customElement } from '../../internal/register-custom-element'; import { LocalizeController } from '../../utilities/localize'; import { property, query, state } from 'lit/decorators.js'; import cx from 'classix'; +import { interactiveStyles } from '../../internal/shared-styles'; import SolidElement from '../../internal/solid-element'; /** @@ -153,6 +154,7 @@ export default class SdBreadcrumb extends SolidElement { static styles = [ ...SolidElement.styles, + interactiveStyles, css` :host { @apply block relative; diff --git a/packages/components/src/components/carousel/carousel.ts b/packages/components/src/components/carousel/carousel.ts index 30ba449337..6d230f01a5 100644 --- a/packages/components/src/components/carousel/carousel.ts +++ b/packages/components/src/components/carousel/carousel.ts @@ -12,6 +12,7 @@ import { range } from 'lit/directives/range.js'; import { ScrollController } from './scroll-controller.js'; import { watch } from '../../internal/watch.js'; import cx from 'classix'; +import { interactiveStyles } from '../../internal/shared-styles'; import SdCarouselItem from '../carousel-item/carousel-item.js'; import SolidElement from '../../internal/solid-element.js'; @@ -834,6 +835,7 @@ export default class SdCarousel extends SolidElement { static styles = [ ...SolidElement.styles, + interactiveStyles, css` :host { --slide-gap: var(--sl-spacing-medium, 1rem); diff --git a/packages/components/src/components/combobox/combobox.ts b/packages/components/src/components/combobox/combobox.ts index 4b568d6f3c..cf6ef431c4 100644 --- a/packages/components/src/components/combobox/combobox.ts +++ b/packages/components/src/components/combobox/combobox.ts @@ -18,6 +18,7 @@ import { unsafeHTML } from 'lit/directives/unsafe-html.js'; import { waitForEvent } from '../../internal/event.js'; import { watch } from '../../internal/watch.js'; import cx from 'classix'; +import { interactiveStyles } from '../../internal/shared-styles'; import SolidElement from '../../internal/solid-element'; import type { SolidFormControl } from '../../internal/solid-element'; import type SdOptgroup from '../optgroup/optgroup.js'; @@ -1569,6 +1570,7 @@ export default class SdCombobox extends SolidElement implements SolidFormControl } static styles = [ ...SolidElement.styles, + interactiveStyles, css` :host { @apply block relative w-full; diff --git a/packages/components/src/components/datepicker/datepicker.ts b/packages/components/src/components/datepicker/datepicker.ts index b4da4e6265..564e484f43 100644 --- a/packages/components/src/components/datepicker/datepicker.ts +++ b/packages/components/src/components/datepicker/datepicker.ts @@ -10,6 +10,7 @@ import { LocalizeController } from '../../utilities/localize'; import { property, query, state } from 'lit/decorators.js'; import { watch } from '../../internal/watch'; import cx from 'classix'; +import { headlineStyles, interactiveStyles } from '../../internal/shared-styles'; import SolidElement from '../../internal/solid-element'; import type { SolidFormControl } from '../../internal/solid-element'; import type SdPopup from '../popup/popup'; @@ -2220,6 +2221,8 @@ export default class SdDatepicker extends SolidElement implements SolidFormContr static styles = [ ...SolidElement.styles, + headlineStyles, + interactiveStyles, css` :host { @apply inline-block relative outline-none w-full; diff --git a/packages/components/src/components/dialog/dialog.ts b/packages/components/src/components/dialog/dialog.ts index 5b6c14eb29..26bfca7a40 100644 --- a/packages/components/src/components/dialog/dialog.ts +++ b/packages/components/src/components/dialog/dialog.ts @@ -13,6 +13,7 @@ import { property, query } from 'lit/decorators.js'; import { waitForEvent } from '../../internal/event'; import { watch } from '../../internal/watch'; import cx from 'classix'; +import { headlineStyles } from '../../internal/shared-styles'; import Modal from '../../internal/modal'; import SolidElement from '../../internal/solid-element'; @@ -338,6 +339,7 @@ export default class SdDialog extends SolidElement { static styles = [ ...SolidElement.styles, + headlineStyles, css` :host { --width: 662px; diff --git a/packages/components/src/components/expandable/expandable.ts b/packages/components/src/components/expandable/expandable.ts index 25a31cd1d0..3696feb3fa 100644 --- a/packages/components/src/components/expandable/expandable.ts +++ b/packages/components/src/components/expandable/expandable.ts @@ -9,6 +9,7 @@ import { property, query } from 'lit/decorators.js'; import { waitForEvent } from '../../internal/event'; import { watch } from '../../internal/watch.js'; import cx from 'classix'; +import { interactiveStyles } from '../../internal/shared-styles'; import SolidElement from '../../internal/solid-element'; /** @@ -187,6 +188,7 @@ export default class SdExpandable extends SolidElement { static styles = [ ...SolidElement.styles, + interactiveStyles, css` :host { --component-expandable-max-block-size: 90px; diff --git a/packages/components/src/components/icon/icon.ts b/packages/components/src/components/icon/icon.ts index e26c46e0cc..7723aaa1d1 100644 --- a/packages/components/src/components/icon/icon.ts +++ b/packages/components/src/components/icon/icon.ts @@ -1,11 +1,11 @@ import { css, html } from 'lit'; -import { customElement } from '../../internal/register-custom-element'; -import { getIconLibrary, unwatchIcon, watchIcon } from './library'; import { property, state } from 'lit/decorators.js'; -import { requestIcon } from './request'; import { unsafeSVG } from 'lit/directives/unsafe-svg.js'; -import { watch } from '../../internal/watch'; +import { customElement } from '../../internal/register-custom-element'; import SolidElement from '../../internal/solid-element'; +import { watch } from '../../internal/watch'; +import { getIconLibrary, unwatchIcon, watchIcon } from './library'; +import { requestIcon } from './request'; let parser: DOMParser; @@ -48,9 +48,12 @@ export default class SdIcon extends SolidElement { */ @property({ reflect: true }) color: 'currentColor' | 'primary' | 'white' = 'currentColor'; + private readonly _boundThemeChange = () => this.setIcon(); + connectedCallback() { super.connectedCallback(); watchIcon(this); + this.renderRoot.addEventListener('sd-theme-change', this._boundThemeChange); } firstUpdated() { @@ -60,10 +63,7 @@ export default class SdIcon extends SolidElement { disconnectedCallback() { super.disconnectedCallback(); unwatchIcon(this); - } - - protected onThemeChange(): void { - this.setIcon(); + this.renderRoot.removeEventListener('sd-theme-change', this._boundThemeChange); } private getUrl() { diff --git a/packages/components/src/components/input/input.ts b/packages/components/src/components/input/input.ts index d86367d519..232e8c0393 100644 --- a/packages/components/src/components/input/input.ts +++ b/packages/components/src/components/input/input.ts @@ -11,6 +11,7 @@ import { longPress } from '../../internal/longpress.js'; import { property, query, state } from 'lit/decorators.js'; import { watch } from '../../internal/watch'; import cx from 'classix'; +import { interactiveStyles } from '../../internal/shared-styles'; import SolidElement from '../../internal/solid-element'; import type { SolidFormControl } from '../../internal/solid-element'; @@ -917,6 +918,7 @@ export default class SdInput extends SolidElement implements SolidFormControl { */ static styles = [ ...SolidElement.styles, + interactiveStyles, css` :host { @apply box-border relative inline-block text-left w-full; diff --git a/packages/components/src/components/navigation-item/navigation-item.ts b/packages/components/src/components/navigation-item/navigation-item.ts index b59ad171a6..1be44fb8c3 100644 --- a/packages/components/src/components/navigation-item/navigation-item.ts +++ b/packages/components/src/components/navigation-item/navigation-item.ts @@ -8,6 +8,7 @@ import { ifDefined } from 'lit/directives/if-defined.js'; import { LocalizeController } from '../../utilities/localize'; import { property, query } from 'lit/decorators.js'; import cx from 'classix'; +import { interactiveStyles } from '../../internal/shared-styles'; import SolidElement from '../../internal/solid-element'; /** @@ -342,6 +343,7 @@ export default class SdNavigationItem extends SolidElement { */ static styles = [ ...SolidElement.styles, + interactiveStyles, css` :host { @apply inline-block relative box-border; diff --git a/packages/components/src/components/scrollable/scrollable.ts b/packages/components/src/components/scrollable/scrollable.ts index 18b2c4f267..7ca4d98c51 100644 --- a/packages/components/src/components/scrollable/scrollable.ts +++ b/packages/components/src/components/scrollable/scrollable.ts @@ -5,6 +5,7 @@ import { ifDefined } from 'lit/directives/if-defined.js'; import { LocalizeController } from '../../utilities/localize'; import { property, query, state } from 'lit/decorators.js'; import cx from 'classix'; +import { interactiveStyles } from '../../internal/shared-styles'; import SolidElement from '../../internal/solid-element'; /** @@ -409,6 +410,7 @@ export default class SdScrollable extends SolidElement { static styles = [ ...SolidElement.styles, + interactiveStyles, css` :host { --gradient-color: transparent; diff --git a/packages/components/src/components/step/step.ts b/packages/components/src/components/step/step.ts index 46e08490cd..655e53e236 100644 --- a/packages/components/src/components/step/step.ts +++ b/packages/components/src/components/step/step.ts @@ -7,6 +7,7 @@ import { ifDefined } from 'lit/directives/if-defined.js'; import { property } from 'lit/decorators.js'; import { watch } from '../../internal/watch'; import cx from 'classix'; +import { paragraphStyles } from '../../internal/shared-styles'; import SolidElement from '../../internal/solid-element'; /** * @summary Steps are used inside [step groups](/components/step-group) to guide users through the steps of a process or task.. @@ -338,6 +339,7 @@ export default class SdStep extends SolidElement { static styles = [ ...SolidElement.styles, + paragraphStyles, css` :host { @apply flex-1; diff --git a/packages/components/src/components/tab-group/tab-group.ts b/packages/components/src/components/tab-group/tab-group.ts index cb333ee68b..91a8e946ec 100644 --- a/packages/components/src/components/tab-group/tab-group.ts +++ b/packages/components/src/components/tab-group/tab-group.ts @@ -7,6 +7,7 @@ import { LocalizeController } from '../../utilities/localize'; import { property, query, state } from 'lit/decorators.js'; import { scrollIntoView } from '../../internal/scroll'; import cx from 'classix'; +import { interactiveStyles } from '../../internal/shared-styles'; import SolidElement from '../../internal/solid-element'; import type SdTab from '../tab/tab'; import type SdTabPanel from '../tab-panel/tab-panel'; @@ -452,6 +453,7 @@ export default class SdTabGroup extends SolidElement { static styles = [ ...SolidElement.styles, + interactiveStyles, css` :host { @apply block box-border; diff --git a/packages/components/src/components/tag/tag.ts b/packages/components/src/components/tag/tag.ts index 34027de2f6..2ca8e37558 100644 --- a/packages/components/src/components/tag/tag.ts +++ b/packages/components/src/components/tag/tag.ts @@ -6,6 +6,8 @@ import { ifDefined } from 'lit/directives/if-defined.js'; import { LocalizeController } from '../../utilities/localize'; import { property, query } from 'lit/decorators.js'; import cx from 'classix'; +import { interactiveStyles } from '../../internal/shared-styles'; +import { token } from '../../internal/token'; import SolidElement from '../../internal/solid-element'; /** @@ -116,7 +118,7 @@ export default class SdTag extends SolidElement { this.emit('sd-hide'); this.style.opacity = '0'; - await new Promise(resolve => setTimeout(resolve, this.token('--sd-duration-fast', 150))); + await new Promise(resolve => setTimeout(resolve, token(this, '--sd-duration-fast', 150))); this.hidden = true; this.emit('sd-after-hide'); @@ -198,6 +200,7 @@ export default class SdTag extends SolidElement { static styles = [ ...SolidElement.styles, + interactiveStyles, css` :host { @apply inline-block transition-opacity duration-fast ease-in-out; diff --git a/packages/components/src/components/teaser-media/teaser-media.ts b/packages/components/src/components/teaser-media/teaser-media.ts index 5bb4028ef4..93dbe000c6 100644 --- a/packages/components/src/components/teaser-media/teaser-media.ts +++ b/packages/components/src/components/teaser-media/teaser-media.ts @@ -3,6 +3,7 @@ import { customElement } from '../../internal/register-custom-element'; import { HasSlotController } from '../../internal/slot'; import { property, query } from 'lit/decorators.js'; import cx from 'classix'; +import { interactiveStyles } from '../../internal/shared-styles'; import SolidElement from '../../internal/solid-element'; /** * @summary Teasers group information into flexible containers so users can browse a collection of related items and actions. @@ -159,6 +160,7 @@ export default class SdTeaserMedia extends SolidElement { static styles = [ ...SolidElement.styles, + interactiveStyles, css` :host { @apply block; diff --git a/packages/components/src/components/tooltip/tooltip.ts b/packages/components/src/components/tooltip/tooltip.ts index ca17bfad62..abd3c9e615 100644 --- a/packages/components/src/components/tooltip/tooltip.ts +++ b/packages/components/src/components/tooltip/tooltip.ts @@ -10,6 +10,7 @@ import { property, query } from 'lit/decorators.js'; import { waitForEvent } from '../../internal/event'; import { watch } from '../../internal/watch'; import cx from 'classix'; +import { interactiveStyles } from '../../internal/shared-styles'; import SolidElement from '../../internal/solid-element'; import type SdPopup from '../popup/popup'; @@ -369,6 +370,7 @@ export default class SdTooltip extends SolidElement { } static styles = [ ...SolidElement.styles, + interactiveStyles, css` :host { --hide-delay: 0ms; diff --git a/packages/components/src/components/video/video.ts b/packages/components/src/components/video/video.ts index 8606ad16a8..5754da7407 100644 --- a/packages/components/src/components/video/video.ts +++ b/packages/components/src/components/video/video.ts @@ -5,6 +5,8 @@ import { HasSlotController } from '../../internal/slot'; import { LocalizeController } from '../../utilities/localize'; import { property, query } from 'lit/decorators.js'; import cx from 'classix'; +import { interactiveStyles } from '../../internal/shared-styles'; +import { token } from '../../internal/token'; import SolidElement from '../../internal/solid-element'; /** @@ -64,7 +66,7 @@ export default class SdVideo extends SolidElement { if (!(this.poster instanceof HTMLImageElement)) return; this.poster.style.opacity = '0'; - await new Promise(resolve => setTimeout(resolve, this.token('--sd-duration-medium', 300))); + await new Promise(resolve => setTimeout(resolve, token(this, '--sd-duration-medium', 300))); this.poster.style.display = 'none'; } @@ -160,6 +162,7 @@ export default class SdVideo extends SolidElement { static styles = [ ...SolidElement.styles, + interactiveStyles, css` :host { @apply relative inline-block overflow-hidden; diff --git a/packages/components/src/internal/shared-styles.ts b/packages/components/src/internal/shared-styles.ts new file mode 100644 index 0000000000..769d760979 --- /dev/null +++ b/packages/components/src/internal/shared-styles.ts @@ -0,0 +1,15 @@ +import { unsafeCSS } from 'lit'; + +const css = unsafeCSS; + +export const interactiveStyles = css` + @import '../styles/src/modules/interactive.css'; +`; + +export const headlineStyles = css` + @import '../styles/src/modules/headline.css'; +`; + +export const paragraphStyles = css` + @import '../styles/src/modules/paragraph.css'; +`; diff --git a/packages/components/src/internal/solid-element.ts b/packages/components/src/internal/solid-element.ts index 48fe3f0c50..8f869a21dc 100644 --- a/packages/components/src/internal/solid-element.ts +++ b/packages/components/src/internal/solid-element.ts @@ -1,42 +1,10 @@ -import { cssVar, parseDuration } from './animate'; import { LitElement, unsafeCSS } from 'lit'; -import { property } from 'lit/decorators.js'; const css = unsafeCSS; -const tokenProcessors: Record string | number> = { - 'sd-duration': (value: string): number => parseDuration(value) -}; - export default class SolidElement extends LitElement { - /** The element's directionality. */ - @property() dir: 'ltr' | 'rtl' | 'auto'; - - /** The element's language. */ - @property() lang: string; - - protected onThemeChange?(e: CustomEvent<{ theme: string }>): void; - - connectedCallback(): void { - super.connectedCallback(); - - if (!this.onThemeChange) return; - this.renderRoot.addEventListener('sd-theme-change', this.onThemeChange.bind(this)); - } - - disconnectedCallback(): void { - super.disconnectedCallback(); - - if (!this.onThemeChange) return; - this.renderRoot.removeEventListener('sd-theme-change', this.onThemeChange.bind(this)); - } - - static styles = [ + static readonly styles = [ css` - @import '../styles/src/modules/interactive.css'; - @import '../styles/src/modules/paragraph.css'; - @import '../styles/src/modules/headline.css'; - :host { /* Add default tailwind variables that get lost during compilation */ --tw-blur: initial; @@ -115,18 +83,6 @@ export default class SolidElement extends LitElement { return event; } - - /** Retrieves the value of a css variable token. */ - token(name: string, fallback: T): T { - const value = cssVar(`var(${name})`, this); - - if (value === null) { - return fallback; - } - - const processor = Object.keys(tokenProcessors).find(token => name.startsWith(token)); - return (tokenProcessors[processor ?? name]?.(value) as T) ?? (value as T) ?? fallback; - } } export interface SolidFormControl extends SolidElement { diff --git a/packages/components/src/internal/token.ts b/packages/components/src/internal/token.ts new file mode 100644 index 0000000000..bd0da00878 --- /dev/null +++ b/packages/components/src/internal/token.ts @@ -0,0 +1,18 @@ +import { cssVar, parseDuration } from './animate'; +import type SolidElement from './solid-element'; + +const tokenProcessors: Record string | number> = { + 'sd-duration': (value: string): number => parseDuration(value) +}; + +/** Retrieves the value of a css variable token from a SolidElement. */ +export function token(el: SolidElement, name: string, fallback: T): T { + const value = cssVar(`var(${name})`, el); + + if (value === null) { + return fallback; + } + + const processor = Object.keys(tokenProcessors).find(t => name.startsWith(t)); + return (tokenProcessors[processor ?? name]?.(value) as T) ?? (value as T) ?? fallback; +} diff --git a/packages/docs/vite.config.js b/packages/docs/vite.config.js index f1324bad75..a3dc2cee71 100644 --- a/packages/docs/vite.config.js +++ b/packages/docs/vite.config.js @@ -1,12 +1,12 @@ -import { processTailwind } from '../components/scripts/esbuild-plugin-lit-tailwind-and-minify.js'; +import VitePluginCustomElementsManifest from 'vite-plugin-cem'; import { replaceCodePlugin as ViteReplaceCodePlugin } from 'vite-plugin-replace'; -import componentsPackageJson from '../components/package.json'; import customElementConfig from '../components/custom-elements-manifest.config.js'; +import componentsPackageJson from '../components/package.json'; +import { processTailwind } from '../components/scripts/esbuild-plugin-lit-tailwind-and-minify.js'; import placeholdersPackageJson from '../placeholders/package.json'; import stylesPackageJson from '../styles/package.json'; import tokensPackageJson from '../tokens/package.json'; import VitePluginCreateEmptyCemIfNotExisting from './scripts/vite-plugin-create-empty-cem-if-not-existing'; -import VitePluginCustomElementsManifest from 'vite-plugin-cem'; import VitePluginFetchIconsFromCdn from './scripts/vite-plugin-fetch-icons-from-cdn'; import VitePluginGetPlaywrightVersion from './scripts/vite-plugin-get-playwright-version'; import VitePluginGetTailwindTheme from './scripts/vite-plugin-get-tailwind-theme'; @@ -21,6 +21,7 @@ export default () => { VitePluginLitTailwind({ include: [ /src\/internal\/solid-element.ts/, + /src\/internal\/shared-styles.ts/, /src\/components\/.*\.ts$/, /src\/utilities\/autocomplete-config.ts/ ],