diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index 8d9a0724cb00..03c9434e49e4 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -10,7 +10,7 @@ }, { "path": "./dist/css/bootstrap-reboot.css", - "maxSize": "5.25 kB" + "maxSize": "5.5 kB" }, { "path": "./dist/css/bootstrap-reboot.min.css", @@ -18,15 +18,15 @@ }, { "path": "./dist/css/bootstrap-utilities.css", - "maxSize": "18.0 kB" + "maxSize": "18.5 kB" }, { "path": "./dist/css/bootstrap-utilities.min.css", - "maxSize": "16.75 kB" + "maxSize": "17.25 kB" }, { "path": "./dist/css/bootstrap.css", - "maxSize": "49.75 kB" + "maxSize": "50.0 kB" }, { "path": "./dist/css/bootstrap.min.css", diff --git a/.cspell.json b/.cspell.json index d11d9df69090..be04ad18f92e 100644 --- a/.cspell.json +++ b/.cspell.json @@ -61,6 +61,7 @@ "jsdelivr", "Jumpstart", "keyframes", + "keyline", "libera", "libman", "Libsass", diff --git a/scss/_config.scss b/scss/_config.scss index 835db327215d..3a2efa1db8ce 100644 --- a/scss/_config.scss +++ b/scss/_config.scss @@ -203,6 +203,7 @@ $paragraph-margin-bottom: 1rem !default; // scss-docs-start border-variables $border-width: 1px !default; $border-widths: ( + keyline: .5px, 1: 1px, 2: 2px, 3: 3px, @@ -213,6 +214,41 @@ $border-style: solid !default; $border-color: color-mix(in oklch, var(--gray-100), var(--gray-200)) !default; // scss-docs-end border-variables +// Each layer's alpha is base * strength * opacity, so shadow-color retints +// (e.g. .shadow-primary), shadow-opacity dims or boosts (.shadow-opacity-*), +// and shadow-strength deepens every shadow in dark mode. Color and opacity +// each read a local, non-inheriting override set by their utility +// (scss/_utilities.scss)—var(--sc, var(--shadow-color)) and var(--so, 1)—so +// neither leaks to nested .shadow-* descendants; --sc falls back to the +// cascading global --shadow-color token (scss/_root.scss) when unset. +// scss-docs-start box-shadow-variables +$shadows: ( + null: #{( + "0 .125rem .25rem -.0625rem oklch(from var(--sc, var(--shadow-color)) l c h / calc(.04 * var(--shadow-strength) * var(--so, 1)))", + "0 .375rem .75rem -.125rem oklch(from var(--sc, var(--shadow-color)) l c h / calc(.05 * var(--shadow-strength) * var(--so, 1)))", + "0 .75rem 1.5rem -.25rem oklch(from var(--sc, var(--shadow-color)) l c h / calc(.06 * var(--shadow-strength) * var(--so, 1)))" + )}, + xs: 0 .0625rem .125rem oklch(from var(--sc, var(--shadow-color)) l c h / calc(.04 * var(--shadow-strength) * var(--so, 1))), + sm: #{( + "0 .0625rem .125rem oklch(from var(--sc, var(--shadow-color)) l c h / calc(.04 * var(--shadow-strength) * var(--so, 1)))", + "0 .125rem .375rem -.0625rem oklch(from var(--sc, var(--shadow-color)) l c h / calc(.05 * var(--shadow-strength) * var(--so, 1)))" + )}, + lg: #{( + "0 .125rem .375rem -.0625rem oklch(from var(--sc, var(--shadow-color)) l c h / calc(.05 * var(--shadow-strength) * var(--so, 1)))", + "0 .375rem 1rem -.1875rem oklch(from var(--sc, var(--shadow-color)) l c h / calc(.06 * var(--shadow-strength) * var(--so, 1)))", + "0 .75rem 2rem -.375rem oklch(from var(--sc, var(--shadow-color)) l c h / calc(.07 * var(--shadow-strength) * var(--so, 1)))", + "0 1.5rem 3.5rem -.75rem oklch(from var(--sc, var(--shadow-color)) l c h / calc(.08 * var(--shadow-strength) * var(--so, 1)))" + )}, + xl: #{( + "0 .1875rem .5rem -.0625rem oklch(from var(--sc, var(--shadow-color)) l c h / calc(.05 * var(--shadow-strength) * var(--so, 1)))", + "0 .5rem 1.25rem -.1875rem oklch(from var(--sc, var(--shadow-color)) l c h / calc(.06 * var(--shadow-strength) * var(--so, 1)))", + "0 1rem 2.5rem -.375rem oklch(from var(--sc, var(--shadow-color)) l c h / calc(.07 * var(--shadow-strength) * var(--so, 1)))", + "0 2rem 5rem -.75rem oklch(from var(--sc, var(--shadow-color)) l c h / calc(.09 * var(--shadow-strength) * var(--so, 1)))" + )}, + inset: inset 0 1px 2px light-dark(oklch(from var(--sc, var(--shadow-color)) l c h / calc(8% * var(--so, 1))), oklch(from var(--sc, var(--shadow-color)) l c h / calc(26% * var(--so, 1)))), +) !default; +// scss-docs-end box-shadow-variables + $transition-base: all .2s ease-in-out !default; $transition-fade: opacity .15s linear !default; diff --git a/scss/_dialog.scss b/scss/_dialog.scss index 69bada97b145..0e9cf1975d67 100644 --- a/scss/_dialog.scss +++ b/scss/_dialog.scss @@ -27,7 +27,7 @@ $dialog-tokens: defaults( --dialog-border-color: var(--border-color-translucent), --dialog-border-width: var(--border-width), --dialog-border-radius: var(--radius-7), - --dialog-box-shadow: var(--box-shadow-lg), + --dialog-box-shadow: var(--box-shadow-xl), --dialog-transition-duration: .3s, --dialog-transition-timing: cubic-bezier(.22, 1, .36, 1), --dialog-backdrop-bg: light-dark(rgb(0 0 0 / 50%), rgb(0 0 0 / 65%)), diff --git a/scss/_drawer.scss b/scss/_drawer.scss index d26bcb793e95..ac45e076d6fb 100644 --- a/scss/_drawer.scss +++ b/scss/_drawer.scss @@ -26,7 +26,7 @@ $drawer-tokens: defaults( --drawer-border-width: var(--border-width), --drawer-border-color: var(--border-color-translucent), --drawer-border-radius: var(--radius-7), - --drawer-box-shadow: var(--box-shadow-lg), + --drawer-box-shadow: var(--box-shadow-xl), --drawer-transition-duration: .3s, --drawer-transition-timing: cubic-bezier(.22, 1, .36, 1), --drawer-title-line-height: 1.5, diff --git a/scss/_menu.scss b/scss/_menu.scss index cddc5ab7de46..31c7853d328e 100644 --- a/scss/_menu.scss +++ b/scss/_menu.scss @@ -23,7 +23,7 @@ $menu-tokens: defaults( // --menu-border-color: var(--border-color-translucent), // --menu-border-radius: var(--radius-7), // --menu-border-width: var(--border-width), - --menu-box-shadow: var(--box-shadow), + --menu-box-shadow: var(--box-shadow-lg), // --menu-max-height: none, --menu-divider-bg: var(--border-color-translucent), --menu-divider-margin-y: .125rem, diff --git a/scss/_root.scss b/scss/_root.scss index 3d53bbd0340c..e013573601ab 100644 --- a/scss/_root.scss +++ b/scss/_root.scss @@ -10,7 +10,7 @@ $root-tokens: () !default; // scss-docs-start root-tokens -// stylelint-disable @stylistic/value-list-max-empty-lines, @stylistic/function-max-empty-lines +// stylelint-disable @stylistic/value-list-max-empty-lines, @stylistic/function-max-empty-lines, custom-property-no-missing-var-function // stylelint-disable-next-line scss/dollar-variable-default $root-tokens: defaults( ( @@ -49,17 +49,17 @@ $root-tokens: defaults( // scss-docs-start root-border-var --border-width: #{$border-width}, + --border-width-keyline: .5px, --border-style: #{$border-style}, --border-color: light-dark(var(--gray-200), var(--gray-700)), --border-color-translucent: color-mix(in oklch, var(--fg-body) 15%, transparent), // scss-docs-end root-border-var // scss-docs-start root-box-shadow-variables - --box-shadow-xs: 0 .0625rem .1875rem rgb(0 0 0 / 7.5%), - --box-shadow-sm: 0 .125rem .25rem rgb(0 0 0 / 7.5%), - --box-shadow: 0 .5rem 1rem rgb(0 0 0 / 15%), - --box-shadow-lg: 0 1rem 3rem rgb(0 0 0 / 17.5%), - --box-shadow-inset: inset 0 1px 2px rgb(0 0 0 / 7.5%), + // --box-shadow* tokens are generated below (see root-box-shadow-loop) from + // the shared $shadows map in scss/_config.scss, which reads these two. + --shadow-color: #000, + --shadow-strength: 1, // scss-docs-end root-box-shadow-variables --spacer: 1rem, @@ -113,7 +113,7 @@ $root-tokens: defaults( ), $root-tokens ); -// stylelint-enable @stylistic/value-list-max-empty-lines, @stylistic/function-max-empty-lines +// stylelint-enable @stylistic/value-list-max-empty-lines, @stylistic/function-max-empty-lines, custom-property-no-missing-var-function // scss-docs-end root-tokens // scss-docs-start root-font-size-loop @@ -169,6 +169,17 @@ $root-tokens: defaults( $root-tokens: map.set($root-tokens, --radius-pill, 50rem); // scss-docs-end root-radius-loop +// Generate box-shadow tokens +// scss-docs-start root-box-shadow-loop +@each $key, $value in $shadows { + @if $key == null { + $root-tokens: map.set($root-tokens, --box-shadow, $value); + } @else { + $root-tokens: map.set($root-tokens, --box-shadow-#{$key}, $value); + } +} +// scss-docs-end root-box-shadow-loop + :root { @include tokens($root-tokens); @@ -178,10 +189,20 @@ $root-tokens: map.set($root-tokens, --radius-pill, 50rem); scrollbar-gutter: stable; } +// Deepen shadows in dark mode. The shadow-strength token is a plain number, so it +// can't use light-dark() (colors only); vary it alongside color-scheme instead. +@media (prefers-color-scheme: dark) { + :root { + --shadow-strength: 2.4; + } +} + [data-bs-theme="dark"] { + --shadow-strength: 2.4; color-scheme: dark; } [data-bs-theme="light"] { + --shadow-strength: 1; color-scheme: light; } diff --git a/scss/_utilities.scss b/scss/_utilities.scss index 96aa6393105e..3d3fd62330b1 100644 --- a/scss/_utilities.scss +++ b/scss/_utilities.scss @@ -9,6 +9,14 @@ // update: // - focus-ring if needed +// Shadow opacity acts as a midpoint: double the standard opacity scale so +// .shadow-opacity-50 → 1 (== the default shadow) and .shadow-opacity-100 → 2 +// (twice as strong). The shadow expressions read a clean var(--shadow-opacity, 1). +$shadow-opacities: () !default; +@each $key, $value in $util-opacity { + $shadow-opacities: map.set($shadow-opacities, $key, $value * 2); +} + $utilities: () !default; // stylelint-disable-next-line scss/dollar-variable-default $utilities: map.merge( @@ -106,15 +114,44 @@ $utilities: map.merge( "shadow": ( property: box-shadow, class: shadow, - values: ( - null: var(--box-shadow), - xs: var(--box-shadow-xs), - sm: var(--box-shadow-sm), - lg: var(--box-shadow-lg), - none: none, - ) + // Pull the literal layered expressions straight from the shared $shadows + // map (single source of truth in scss/_config.scss, also consumed by + // scss/_root.scss). Emitting the literal—rather than var(--box-shadow-*)— + // keeps the nested var(--sc, var(--shadow-color)) / var(--so, 1) + // unresolved so they resolve on the element, letting .shadow-{color} and + // .shadow-opacity-* take effect. `inset` isn't a utility, so it's excluded. + values: map.merge(map.remove($shadows, inset), (none: none)) ), // scss-docs-end utils-shadow + // scss-docs-start utils-shadow-color + "shadow-color": ( + // --sc is a local override composed into the $shadows expressions + // (scss/_config.scss) as var(--sc, var(--shadow-color)); like --bc for + // border-color, it's non-inheriting so .shadow-primary doesn't tint + // nested .shadow-* descendants, which instead fall back to the + // cascading global --shadow-color token (scss/_root.scss). + property: --sc, + class: shadow, + values: map-merge-multiple( + theme-color-refs("base"), + ( + "current": currentcolor, + "black": var(--black), + "white": var(--white), + ) + ) + ), + // scss-docs-end utils-shadow-color + // scss-docs-start utils-shadow-opacity + "shadow-opacity": ( + // --so is a local override composed into the $shadows expressions + // (scss/_config.scss) as var(--so, 1); same rationale as --sc above, + // it's non-inheriting so it doesn't leak to nested .shadow-* descendants. + property: --so, + class: shadow-opacity, + values: $shadow-opacities + ), + // scss-docs-end utils-shadow-opacity // scss-docs-start utils-position "position": ( property: position, @@ -225,7 +262,7 @@ $utilities: map.merge( "border-width": ( property: border-width, class: border, - values: $border-widths + values: map.merge($border-widths, (keyline: var(--border-width-keyline))) ), "border-opacity": ( class: border, diff --git a/site/src/components/header/Navigation.astro b/site/src/components/header/Navigation.astro index 9bc8c037ae5d..165af1caf1c9 100644 --- a/site/src/components/header/Navigation.astro +++ b/site/src/components/header/Navigation.astro @@ -45,7 +45,7 @@ const activeSection = !layout ? 'Home' : title === 'Examples' ? 'Examples' : 'Do {activeSection} -
`} /> + +### Shadow opacity + +Scale a shadow’s strength with `.shadow-opacity-{10-100}` utilities. Opacity is treated as a midpoint, not a ceiling: the default shadow matches `.shadow-opacity-50`, so lower values lighten it while higher values—up to `.shadow-opacity-100`—deepen it. This works with any size or color utility. Like `--bs-sc`, `--bs-so` is scoped to the element, so pair it with a size or color utility on the same element rather than expecting it to inherit. + +