diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index 53a01dd33397..6995d6a0aa01 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -26,11 +26,11 @@ }, { "path": "./dist/css/bootstrap.css", - "maxSize": "38.0 kB" + "maxSize": "38.75 kB" }, { "path": "./dist/css/bootstrap.min.css", - "maxSize": "36.75 kB" + "maxSize": "37.25 kB" }, { "path": "./dist/js/bootstrap.bundle.js", diff --git a/scss/_stepper.scss b/scss/_stepper.scss new file mode 100644 index 000000000000..00041acff3c9 --- /dev/null +++ b/scss/_stepper.scss @@ -0,0 +1,143 @@ +@use "sass:map"; +@use "config" as *; +@use "variables" as *; +@use "layout/breakpoints" as *; +@use "mixins/border-radius" as *; +@use "mixins/box-shadow" as *; +@use "mixins/gradients" as *; +@use "mixins/transition" as *; + +// scss-docs-start stepper-variables +$stepper-size: 2rem !default; +$stepper-gap: 1rem !default; +$stepper-track-size: .25rem !default; +$stepper-bg: var(--bg-2) !default; +$stepper-active-fg: var(--primary-contrast) !default; +$stepper-active-bg: var(--primary-bg) !default; +// $stepper-vertical-gap: .5rem !default; +// scss-docs-end stepper-variables + +// scss-docs-start stepper-horizontal-mixin +@mixin stepper-horizontal() { + display: inline-grid; + grid-auto-columns: 1fr; + grid-auto-flow: column; + + .stepper-item { + grid-template-rows: repeat(2, var(--stepper-size)); + grid-template-columns: auto; + justify-items: center; + + &::after { + top: calc((var(--stepper-size) * .5) - (var(--stepper-track-size) * .5)); + right: 0; + bottom: auto; + left: calc(-50% - var(--stepper-gap)); + width: auto; + height: var(--stepper-track-size); + } + + &:last-child::after { + right: 50%; + } + } +} +// scss-docs-end stepper-horizontal-mixin + +// scss-docs-start stepper-css +.stepper { + // scss-docs-start stepper-css-vars + --stepper-size: #{$stepper-size}; + --stepper-gap: #{$stepper-gap}; + --stepper-bg: #{$stepper-bg}; + --stepper-track-size: #{$stepper-track-size}; + --stepper-active-color: #{$stepper-active-fg}; + --stepper-active-bg: #{$stepper-active-bg}; + // scss-docs-end stepper-css-vars + + display: grid; + grid-auto-rows: 1fr; + grid-auto-flow: row; + gap: var(--stepper-gap); + padding-left: 0; + list-style: none; + counter-reset: stepper; +} + +.stepper-item { + position: relative; + display: grid; + grid-template-rows: auto; + grid-template-columns: var(--stepper-size) auto; + gap: .5rem; + place-items: center; + justify-items: start; + text-align: center; + text-decoration: none; + + + // The counter + &::before { + position: relative; + z-index: 1; + display: inline-block; + width: var(--stepper-size); + height: var(--stepper-size); + padding: .5rem; + font-weight: 600; + line-height: 1; + text-align: center; + content: counter(stepper); + counter-increment: stepper; + background-color: var(--stepper-bg); + @include border-radius(50%); + } + + // Connecting lines + &::after { + position: absolute; + top: calc(var(--stepper-gap) * -1); + bottom: 100%; + left: calc((var(--stepper-size) * .5) - (var(--stepper-track-size) * .5)); + width: var(--stepper-track-size); + content: ""; + background-color: var(--stepper-bg); + } + + // Avoid sibling selector for easier CSS overrides + &:first-child::after { + display: none; + } + + &.active { + &::before, + &::after { + color: var(--theme-contrast, var(--stepper-active-color)); + background-color: var(--theme-bg, var(--stepper-active-bg)); + } + } +} + +@each $breakpoint in map.keys($grid-breakpoints) { + @include media-breakpoint-up($breakpoint) { + $infix: breakpoint-infix($breakpoint, $grid-breakpoints); + + .stepper-horizontal#{$infix} { + @include stepper-horizontal(); + } + } +} + +// scss-docs-start stepper-overflow +.stepper-overflow { + container-type: inline-size; + overflow-x: auto; + overscroll-behavior-x: contain; + -webkit-overflow-scrolling: touch; + + > .stepper { + width: max-content; + min-width: 100%; + } +} +// scss-docs-end stepper-overflow diff --git a/scss/bootstrap.scss b/scss/bootstrap.scss index 764d9f3149a8..167095d06395 100644 --- a/scss/bootstrap.scss +++ b/scss/bootstrap.scss @@ -30,6 +30,7 @@ @forward "popover"; @forward "progress"; @forward "spinner"; +@forward "stepper"; @forward "toasts"; @forward "tooltip"; @forward "transitions"; diff --git a/site/data/sidebar.yml b/site/data/sidebar.yml index ca6b349be9a6..3bb8358d07a0 100644 --- a/site/data/sidebar.yml +++ b/site/data/sidebar.yml @@ -107,6 +107,7 @@ - title: Progress - title: Scrollspy - title: Spinner + - title: Stepper - title: Toasts - title: Toggler - title: Tooltip diff --git a/site/src/components/shortcodes/ButtonPlayground.astro b/site/src/components/shortcodes/ButtonPlayground.astro index 7d8d59043045..359e4d011454 100644 --- a/site/src/components/shortcodes/ButtonPlayground.astro +++ b/site/src/components/shortcodes/ButtonPlayground.astro @@ -25,7 +25,7 @@ const rounded = ['default', 'pill', 'square'] -
+
@@ -38,7 +38,7 @@ const rounded = ['default', 'pill', 'square'] aria-expanded="false" data-color="primary" > - Primary + Primary @@ -89,7 +89,7 @@ const rounded = ['default', 'pill', 'square'] aria-expanded="false" data-size="" > - Medium + Medium @@ -270,7 +270,8 @@ const rounded = ['default', 'pill', 'square'] const colorTitle = item.textContent?.trim() || 'Primary' // Update button text and data attribute - colorDropdownButton.textContent = colorTitle + const labelSpan = colorDropdownButton.querySelector('span') + if (labelSpan) labelSpan.textContent = colorTitle colorDropdownButton.dataset.color = colorName // Update selected state @@ -301,7 +302,8 @@ const rounded = ['default', 'pill', 'square'] const sizeLabel = item.textContent?.trim() || 'Medium' // Update button text and data attribute - sizeDropdownButton.textContent = sizeLabel + const labelSpan = sizeDropdownButton.querySelector('span') + if (labelSpan) labelSpan.textContent = sizeLabel sizeDropdownButton.dataset.size = sizeValue // Update selected state diff --git a/site/src/components/shortcodes/DropdownPlacementPlayground.astro b/site/src/components/shortcodes/DropdownPlacementPlayground.astro index 5528d7c9178c..e37c08e52c63 100644 --- a/site/src/components/shortcodes/DropdownPlacementPlayground.astro +++ b/site/src/components/shortcodes/DropdownPlacementPlayground.astro @@ -28,7 +28,7 @@ const logicalPlacements = [ ] --- -
+
@@ -67,7 +67,10 @@ const logicalPlacements = [ data-placement="bottom-start" style="min-width: 160px;" > - bottom-start + bottom-start +
`} /> + +## Playground + +Experiment with stepper options including orientation, breakpoints, step count, and more. + + + +### Alignment + +Use [text alignment utilities]([[docsref:/utilities/text-alignment]]) (because we use `display: inline-grid`) to align the steps. The inline grid arrangement allows us to keep the steps equal width and ensures the connecting lines are rendered correctly. + + +
  • Default stepper
  • +
  • Confirm email
  • +
  • Update profile
  • +
  • Finish
  • + `} /> + + +
  • Center stepper
  • +
  • Confirm email
  • +
  • Update profile
  • +
  • Finish
  • + `} /> + + +
  • End stepper
  • +
  • Confirm email
  • +
  • Update profile
  • +
  • Finish
  • + `} /> + +Apply `.w-100` to the stepper to make it full width. Stepper items will be stretched to fill the available space. Alignment doesn't affect full-width steppers. + + +
  • Create account
  • +
  • Confirm email
  • +
  • Update profile
  • +
  • Finish
  • + `} /> + +### With anchors + +Use anchor elements to build your stepper if it links across multiple pages. Add `role="button"` or use `
    `} /> + +## CSS + +### Variables + +Steppers use [CSS variables]([[docsref:/customize/css-variables]]) for easier customization. + + + +### Sass variables + + + +### Sass mixin + + + +### Overflow wrapper + + diff --git a/site/src/types/auto-import.d.ts b/site/src/types/auto-import.d.ts index 2a0cd6f4a019..3019f6b46cbe 100644 --- a/site/src/types/auto-import.d.ts +++ b/site/src/types/auto-import.d.ts @@ -21,6 +21,7 @@ export declare global { export const JsDocs: typeof import('@shortcodes/JsDocs.astro').default export const Placeholder: typeof import('@shortcodes/Placeholder.astro').default export const ScssDocs: typeof import('@shortcodes/ScssDocs.astro').default + export const StepperPlayground: typeof import('@shortcodes/StepperPlayground.astro').default export const Swatch: typeof import('@shortcodes/Swatch.astro').default export const Table: typeof import('@shortcodes/Table.astro').default }