Migrate to Bootstrap 5 and remove Vue compat plugin
Complete migration from Bootstrap 4 (bootstrap-vue) to Bootstrap 5
(bootstrap-vue-next) and remove the @vue/compat plugin to finalize
the Vue 3 migration.
Bundle size impact:
- Before (Bootstrap 4 + bootstrap-vue): 535 KiB gzipped
- After (Bootstrap 5 + bootstrap-vue-next): 511 KiB gzipped
- Reduction: 24 KiB (4.5% smaller)
Package updates:
- Update bootstrap 4.6.2 -> 5.3.8
- Update bootstrap-vue 2.23.1 -> bootstrap-vue-next 0.40.8
- Remove @vue/compat plugin
- Update vue 3.4.29 -> 3.5.24 and related packages
- Add mitt 3.0.1 for global event bus
- Add vue-demi 0.14.10 for library compatibility
Bootstrap 5 CSS updates:
- Replace directional classes: ml/mr/pl/pr -> ms/me/ps/pe
- Replace text-left/right -> text-start/end
- Replace sr-only -> visually-hidden / visually-hidden-focusable
- Update media breakpoint xs -> sm (Bootstrap 5 removed xs)
- Update color functions: gray("700") -> $gray-700
- Add form-switch border-radius for curved toggles
- Update alert, table, toast, form, and button styles
Bootstrap-Vue-Next API changes:
- Use createBootstrap() for plugin registration
- Update modal footer slots: #modal-footer -> #footer
- Fix form select events: @change -> @update:model-value
- Add v-model bindings to modals instead of manual show()/hide()
- Update toast system with custom plugin wrapping useToast()
- Register components and directives explicitly
Vue 3 specific updates:
- Replace $root.$emit with mitt event bus (eventBus.js)
- Update render function from h(App) to createApp(App)
- Add emits option to components
- Use h() instead of $createElement in mixins
- Add Vue 3 compile-time feature flags with documentation
- Update event listeners: $on/$off to eventBus methods
- Add beforeUnmount cleanup for event listeners
New components and significant additions:
- src/plugins/toast.js - Custom toast plugin wrapping useToast() for
Options API compatibility
- src/components/Global/ConfirmModal.vue - Global confirmation dialog
shim to replace Bootstrap 4's removed bvModal.msgBoxConfirm
- src/eventBus.js - mitt-based event bus with Vue 2-compatible API
- Navigation state preservation on page refresh implemented
Critical fixes:
- Add global API interceptor to strip Vue reactivity from payloads
- Preserve binary data (File, Blob, FormData) in API requests
- Fix Generate CSR modal v-model binding for proper open/close
- Remove debug logging and fix jest configuration
- Fix responsive text visibility in AppHeader
- Update BVTableSelectableMixin for proper row selection
- Fix BVToastMixin VNode rendering for Vue 3
Vue 3 modal fixes (lazy-loaded components):
- Add v-model support to network modals (ModalIpv4, ModalIpv6, ModalDns,
ModalHostname, ModalMacAddress, ModalDefaultGateway) by adding
modelValue prop, watcher on modelValue that triggers show(), and
update:modelValue emit in resetForm
- Remove lazy loading from TableIpv4, TableIpv6, TableDns to ensure
modal component refs are available when v-model triggers
- Fix modal title accessibility by adding title prop to modals
(ModalAddDestination, ModalUser, ModalAddRoleGroup, etc.)
i18n fixes (computed properties):
- Fix computed properties using i18n translations in ModalAddRoleGroup,
ModalUser, and ModalUploadCertificate
- Move useI18n() call from data() to setup() and return i18n object
- Use i18n.t() instead of $t in computed properties and templates
- Prevents "this.$t is not a function" and "_ctx.$t is not a function"
errors in Vue 3
Toast notification fixes:
- Fix toast progress bar visibility by setting progressProps to
undefined (documented way to opt-out) instead of false
- Change modelValue prop to interval for auto-dismiss timing
- Remove temporary CSS display:none hack from _toasts.scss
Network settings fixes:
- Fix checkbox @change event sending Vue reactive proxy object instead
of boolean by casting with !! operator in changeDomainNameState and
related methods in NetworkGlobalSettings.vue
- Ensures API receives plain boolean values in PATCH requests
Navigation fixes:
- Fix nav-link styling for navigation items without children by
replacing b-nav-item with router-link in AppNavigation.vue
- Prevents blue font color from .nav-link CSS class
Configuration updates:
- Remove vue-compat webpack configuration
- Add Vue 3 feature flags (__VUE_OPTIONS_API__, etc.)
- Add .cursor to .gitignore
Accessibility improvements:
- Add autocomplete attributes to password and credential inputs
- Add modal title props for screen reader support
Build completes successfully and UI behavior matches pre-migration.
Extracted features (to be submitted in follow-up PRs):
The following features were removed from this migration PR to keep it
focused on the Bootstrap 5 upgrade. They will be submitted separately:
1. UnresponsiveModal - Server connectivity watchdog with auto-retry
2. Auth token persistence - sessionStorage support for X-Auth-Token
3. Hardware store error handling - try/catch, dynamic discovery
4. Login page connecting indicator - Backend polling with spinner
5. Test updates - Jest setup and snapshot updates for
Bootstrap-Vue-Next
6. Documentation updates - Vue 3 and Vue I18n v9+ API documentation
7. Enhanced ConfirmModal - Feature-rich confirmation dialog with
custom actions
Change-Id: Ib76a58f324b3c926cf536e6e4626e4271639de38
Signed-off-by: Jason Westover <jwestover@nvidia.com>
diff --git a/src/assets/styles/_obmc-custom.scss b/src/assets/styles/_obmc-custom.scss
index 068364c..645d2b3 100644
--- a/src/assets/styles/_obmc-custom.scss
+++ b/src/assets/styles/_obmc-custom.scss
@@ -1,6 +1,5 @@
// Vendor styles
@import "./bootstrap";
-@import "~bootstrap-vue/src/index";
// Custom BMC styles
@import "./bmc/custom";
diff --git a/src/assets/styles/bmc/custom/_alert.scss b/src/assets/styles/bmc/custom/_alert.scss
index 0e78ba6..c7911fe 100644
--- a/src/assets/styles/bmc/custom/_alert.scss
+++ b/src/assets/styles/bmc/custom/_alert.scss
@@ -1,12 +1,13 @@
+
.alert {
display: flex;
padding: $spacer;
- border-width: 0 0 0 3px;
- color: gray("800");
+ border-width: 0 0 0 3px; // keep physical width for browsers; color uses logical start
+ color: $gray-800;
margin-bottom: $spacer;
&.small {
- padding: $spacer / 2;
+ padding: calc($spacer / 2);
font-size: 1rem;
}
@@ -14,56 +15,66 @@
font-weight: 300;
opacity: 1;
}
+}
- .alert-icon {
- display: inline-flex;
- align-items: flex-start;
- margin-right: $spacer;
- margin-bottom: $spacer;
+// Bootstrap 5 alert structure has deeper nesting - use descendant selectors
+.alert .alert-body{
+ display: inline-flex;
+ flex-direction: row;
+}
- @include media-breakpoint-up(sm) {
- margin-bottom: 0;
- }
- }
+.alert .alert-icon {
+ display: inline-flex;
+ align-items: flex-start;
+ margin-inline-end: $spacer;
+ margin-bottom: $spacer;
- .alert-content {
- flex: 1 1 auto;
- }
+ @include media-breakpoint-up(sm) {
+ margin-bottom: 0;
+ }
+}
- .alert-title {
- margin-bottom: $spacer / 2;
- }
+.alert .alert-content {
+ flex: 1 1 auto;
+}
- .alert-msg {
- p + p {
- margin-bottom: $spacer;
- }
+.alert .alert-title {
+ margin-bottom: calc($spacer / 2);
+}
- p:last-of-type {
- margin-bottom: 0;
- }
- }
+.alert .alert-msg {
+ p + p {
+ margin-bottom: $spacer;
+ }
+
+ p:last-of-type {
+ margin-bottom: 0;
+ }
+}
+
+// Alert variant styles
+.alert {
&.alert-info {
- border-left-color: theme-color("info");
+ border-inline-start-color: theme-color("info");
background-color: theme-color-light("info");
fill: theme-color("info");
}
&.alert-success {
- border-left-color: theme-color("success");
+ border-inline-start-color: theme-color("success");
background-color: theme-color-light("success");
fill: theme-color("success");
}
&.alert-danger {
- border-left-color: theme-color("danger");
+ border-inline-start-color: theme-color("danger");
background-color: theme-color-light("danger");
fill: theme-color("danger");
}
&.alert-warning {
- border-left-color: theme-color("warning");
+ border-inline-start-color: theme-color("warning");
background-color: theme-color-light("warning");
fill: theme-color("warning");
}
diff --git a/src/assets/styles/bmc/custom/_badge.scss b/src/assets/styles/bmc/custom/_badge.scss
index 0b88b49..01d05a8 100644
--- a/src/assets/styles/bmc/custom/_badge.scss
+++ b/src/assets/styles/bmc/custom/_badge.scss
@@ -1,3 +1,4 @@
+
.badge-pill {
// Need to explicitly set border-radius
// for pill variant because global $enable-rounded
@@ -9,7 +10,7 @@
display: inline-flex;
.close {
font-size: 1em;
- margin-left: $spacer/2;
+ margin-inline-start: calc($spacer / 2);
font-weight: inherit;
color: inherit;
}
diff --git a/src/assets/styles/bmc/custom/_base.scss b/src/assets/styles/bmc/custom/_base.scss
index c11e046..61f5813 100644
--- a/src/assets/styles/bmc/custom/_base.scss
+++ b/src/assets/styles/bmc/custom/_base.scss
@@ -1,7 +1,7 @@
dt,
legend,
label {
- color: gray("800");
+ color: $gray-800;
font-size: 14px;
font-weight: 400;
line-height: 1.4285;
diff --git a/src/assets/styles/bmc/custom/_bootstrap-grid.scss b/src/assets/styles/bmc/custom/_bootstrap-grid.scss
index 7ad7c81..c2cf8b6 100644
--- a/src/assets/styles/bmc/custom/_bootstrap-grid.scss
+++ b/src/assets/styles/bmc/custom/_bootstrap-grid.scss
@@ -4,5 +4,5 @@
// is set, setting the left margin to 0 is needed
// so the content doesn't center align
// https://bootstrap-vue.org/docs/components/layout#fluid-width-container
- margin-left: 0;
+ margin-inline-start: 0;
}
\ No newline at end of file
diff --git a/src/assets/styles/bmc/custom/_buttons.scss b/src/assets/styles/bmc/custom/_buttons.scss
index 0ab7baa..008f08d 100644
--- a/src/assets/styles/bmc/custom/_buttons.scss
+++ b/src/assets/styles/bmc/custom/_buttons.scss
@@ -1,23 +1,23 @@
@import 'bootstrap/dist/css/bootstrap.css';
.btn {
- padding-top: $spacer / 2;
- padding-right: $spacer;
- padding-bottom: $spacer / 2;
- padding-left: $spacer;
+ padding-top: calc($spacer / 2);
+ padding-inline-end: $spacer;
+ padding-bottom: calc($spacer / 2);
+ padding-inline-start: $spacer;
display: inline-flex;
align-items: center;
justify-content: space-around;
svg {
- margin-right: $spacer / 4;
+ margin-inline-end: calc($spacer / 4);
}
&:disabled {
- color: gray("600");
+ color: $gray-600;
fill: currentColor;
box-shadow: none !important;
&:not(.btn-link) {
- border-color: gray("400");
- background-color: gray("400");
+ border-color: $gray-400;
+ background-color: $gray-400;
}
}
}
@@ -46,11 +46,11 @@
fill: theme-color("primary");
text-decoration: none !important;
&:hover {
- background-color: gray("200");
+ background-color: $gray-200;
color: theme-color("primary");
}
&:active {
- background-color: gray("300");
+ background-color: $gray-300;
}
&:focus {
box-shadow: inset 0 0 0 2px theme-color("primary");
@@ -64,14 +64,14 @@
// Icon only buttons
.btn-icon-only svg {
- margin-right: 0;
+ margin-inline-end: 0;
}
// Datepicker, clear search and Password toggle buttons
.input-action-btn,
.btn-datepicker {
position: absolute;
- right: 0;
+ inset-inline-end: 0;
top: 0;
z-index: $zindex-dropdown + 1;
}
diff --git a/src/assets/styles/bmc/custom/_dropdown.scss b/src/assets/styles/bmc/custom/_dropdown.scss
index 82dda86..5f90fac 100644
--- a/src/assets/styles/bmc/custom/_dropdown.scss
+++ b/src/assets/styles/bmc/custom/_dropdown.scss
@@ -1,26 +1,28 @@
+
// Make calendar visible over the table
.dropdown-menu {
z-index: $zindex-dropdown + 1;
padding: 0;
}
.dropdown-item {
- padding-left: $spacer/4;
- margin-top: -1 * $spacer/4;
+ padding-inline-start: calc(#{$spacer} / 4);
+ margin-top: calc(-1 * (#{$spacer} / 4));
}
.b-dropdown-form {
- padding: $spacer/2;
+ padding: calc(#{$spacer} / 2);
+ // Maintain .form-group spacing for backward compatibility
.form-group {
- margin-bottom: $spacer/2;
+ margin-bottom: calc(#{$spacer} / 2);
}
}
// Table filter dropdown clear button style
.table-filter {
.dropdown-item {
&:hover {
- background-color: gray("200");
+ background-color: $gray-200;
}
&:active {
- background-color: gray("300");
+ background-color: $gray-300;
}
&:focus {
outline: none;
diff --git a/src/assets/styles/bmc/custom/_forms.scss b/src/assets/styles/bmc/custom/_forms.scss
index 428a40c..8c262a6 100644
--- a/src/assets/styles/bmc/custom/_forms.scss
+++ b/src/assets/styles/bmc/custom/_forms.scss
@@ -1,46 +1,53 @@
-// Helper text
-.form-text {
+
+// Helper text (Bootstrap 4 and bootstrap-vue-next)
+.form-text,
+.b-form-text {
+ display: block;
font-size: $form-label-font-size;
line-height: $form-line-height;
- margin-top: -$spacer / 4;
- margin-bottom: $spacer / 2;
- color: gray("700")!important;
+ margin-top: calc(-1 * (#{$spacer} / 4));
+ margin-bottom: calc(#{$spacer} / 2);
+ color: $gray-700!important;
}
// Legend label
.col-form-label {
- color: gray("800");
+ color: $gray-800;
font-size: $form-label-font-size;
line-height: $form-line-height;
}
-.form-group {
+// Bootstrap 4 and bootstrap-vue-next form group spacing
+.form-group,
+.b-form-group {
margin-bottom: $spacer * 2;
}
.custom-select,
+.form-select,
.form-control,
.input-group-text {
- border-color: gray("500") !important;
- background-color: gray("100");
+ border-color: $gray-500 !important;
+ background-color: $gray-100;
}
.custom-select,
+.form-select,
.form-control {
&:active {
border: 1px solid $primary!important;
}
&:focus {
color: theme-color("dark");
- background-color: gray("100");
- box-shadow: inset 0 0 0 3px gray("100"), inset 0 0 0 5px $primary !important;
+ background-color: $gray-100;
+ box-shadow: inset 0 0 0 3px $gray-100, inset 0 0 0 5px $primary !important;
}
&:disabled {
- background-color: gray("400");
- color: gray("600");
+ background-color: $gray-400;
+ color: $gray-600;
}
&::placeholder {
- color: gray("600");
+ color: $gray-600;
}
&.is-invalid,
&:invalid {
@@ -49,7 +56,9 @@
}
.custom-select,
+.form-select,
.custom-control-label,
+.form-check-label,
.form-control {
color: theme-color("dark") !important;
font-size: 1rem;
@@ -57,16 +66,17 @@
// Inverted form colors
.form-background {
- background-color: gray("100");
+ background-color: $gray-100;
.custom-select,
+ .form-select,
.form-control {
background-color: $white;
&:focus {
background-color: $white;
}
&:disabled {
- background-color: gray("400");
- color: gray("600");
+ background-color: $gray-400;
+ color: $gray-600;
}
}
}
@@ -76,9 +86,12 @@
line-height: $form-line-height;
}
+// Checkbox and radio button styling with backward compatibility
.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after,
.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before,
-.custom-control-input:checked ~ .custom-control-label::before {
+.custom-control-input:checked ~ .custom-control-label::before,
+.form-check-input:checked,
+.form-check-input:indeterminate {
background-color: $black;
border-color: $black;
cursor: pointer;
@@ -89,7 +102,17 @@
& + .custom-control-label {
// Disabled label for checkbox, radio,
// switch bootstrap form components
- color: gray("600")!important;
+ color: $gray-600!important;
+ }
+ }
+}
+
+.form-check {
+ .form-check-input[disabled=disabled] {
+ & + .form-check-label {
+ // Disabled label for checkbox, radio,
+ // switch bootstrap form components
+ color: $gray-600!important;
}
}
}
@@ -98,7 +121,12 @@
box-shadow: 0 0 0 2px theme-color("primary");
}
-.custom-control-label::after {
+.form-check-input:focus {
+ box-shadow: 0 0 0 2px theme-color("primary");
+}
+
+.custom-control-label::after,
+.form-check-label {
cursor: pointer;
}
@@ -110,7 +138,7 @@
.b-form-tags-button {
// Add button inside input field
white-space: nowrap;
- margin-right: -$spacer;
+ margin-inline-end: -$spacer;
&.btn-link-primary {
color: theme-color("primary");
fill: currentColor;
@@ -130,3 +158,8 @@
background-position: right 3rem bottom 50%;
}
}
+
+// Bootstrap 5 form-switch styling for curved toggle switches
+.form-switch .form-check-input {
+ border-radius: 1.5625rem; // 2em equivalent to match Bootstrap 5's $form-switch-border-radius
+}
diff --git a/src/assets/styles/bmc/custom/_modal.scss b/src/assets/styles/bmc/custom/_modal.scss
index e2fa0cd..d55e8b4 100644
--- a/src/assets/styles/bmc/custom/_modal.scss
+++ b/src/assets/styles/bmc/custom/_modal.scss
@@ -1,7 +1,6 @@
.modal-header {
- .close {
+ .btn-close {
font-weight: normal;
- color: theme-color("dark");
opacity: 1;
}
.modal-title {
diff --git a/src/assets/styles/bmc/custom/_pagination.scss b/src/assets/styles/bmc/custom/_pagination.scss
index d38ce5d..662af0f 100644
--- a/src/assets/styles/bmc/custom/_pagination.scss
+++ b/src/assets/styles/bmc/custom/_pagination.scss
@@ -6,7 +6,7 @@
width: fit-content;
}
label {
- margin-left: $spacer;
+ margin-inline-start: $spacer;
line-height: $spacer * 2;
}
}
diff --git a/src/assets/styles/bmc/custom/_section-divider.scss b/src/assets/styles/bmc/custom/_section-divider.scss
index 620c9e5..bed2e1a 100644
--- a/src/assets/styles/bmc/custom/_section-divider.scss
+++ b/src/assets/styles/bmc/custom/_section-divider.scss
@@ -1,3 +1,3 @@
.section-divider {
- border-bottom: 1px solid gray('400');
+ border-bottom: 1px solid $gray-400;
}
\ No newline at end of file
diff --git a/src/assets/styles/bmc/custom/_tables.scss b/src/assets/styles/bmc/custom/_tables.scss
index e8b5a83..d6d5a74 100644
--- a/src/assets/styles/bmc/custom/_tables.scss
+++ b/src/assets/styles/bmc/custom/_tables.scss
@@ -1,39 +1,43 @@
+
.table {
position: relative;
z-index: $zindex-dropdown;
td {
- border-top: 1px solid gray("300");
- border-bottom: 1px solid gray("300");
+ border-top: 1px solid $gray-300;
+ border-bottom: 1px solid $gray-300;
&:first-of-type {
- border-left: 1px solid gray("300");
+ border-inline-start: 1px solid $gray-300;
}
&:last-of-type {
- border-right: 1px solid gray("300");
+ border-inline-end: 1px solid $gray-300;
}
vertical-align: middle;
// Table action buttons
.btn-link {
- width: 40px;
- height: 40px;
- padding: 5px !important;
+ padding: 0.25rem 0.5rem !important;
display: inline-flex;
justify-content: center;
align-items: center;
+ line-height: 1;
+ .visually-hidden {
+ margin-inline-start: 0.25rem;
+ }
}
}
- // thead-light added for specificity
- .thead-light th {
+ // Bootstrap 4 thead-light and Bootstrap 5 table-light classes for specificity
+ .thead-light th,
+ .table-light th {
vertical-align: middle;
- border-top: 1px solid gray("300");
- border-bottom: 1px solid gray("300");
+ border-top: 1px solid $gray-300;
+ border-bottom: 1px solid $gray-300;
&:first-of-type {
- border-left: 1px solid gray("300");
+ border-inline-start: 1px solid $gray-300;
}
&:last-of-type {
- border-right: 1px solid gray("300");
+ border-inline-end: 1px solid $gray-300;
}
color: theme-color("dark");
&:focus {
@@ -50,24 +54,26 @@
td {
border-bottom: none;
}
- .table-row-expand svg {
- transform: rotate(180deg);
- }
+ }
+
+ // Show right-pointing chevron when collapsed
+ .table-row-expand .btn.collapsed svg {
+ transform: rotate(-90deg);
}
.b-table-details {
background-color: theme-color("light");
td {
- padding-left: calc(50px + (#{$table-cell-padding} * 2));
- padding-right: calc(50px + (#{$table-cell-padding} * 2));
+ padding-inline-start: calc(50px + (#{$table-cell-padding-x} * 2));
+ padding-inline-end: calc(50px + (#{$table-cell-padding-x} * 2));
}
dl {
margin: 0;
}
dt {
- float: left;
- clear: left;
- margin-right: $spacer / 2;
+ float: inline-start;
+ clear: inline-start;
+ margin-inline-end: calc($spacer / 2);
}
dd {
line-height: 1.2
@@ -78,7 +84,6 @@
width: 50px;
.btn {
padding: 0;
- width: 50px;
}
svg {
fill: theme-color("dark");
@@ -86,7 +91,7 @@
}
.b-table-sort-icon-left {
background-position: left calc(1.5rem / 2) center !important;
- padding-left: calc(1.2rem + 0.65em) !important;
+ padding-inline-start: calc(1.2rem + 0.65em) !important;
&:focus {
outline: none;
box-shadow: inset 0 0 0 2px theme-color('primary') !important;
@@ -101,15 +106,15 @@
border-top: none;
}
-// Table stacked style for small screen only
-@include media-breakpoint-down(xs) {
+// Table stacked style for small screens (< 576px)
+@include media-breakpoint-down(sm) {
.b-table-stacked-sm {
- border: 1px solid gray("300");
+ border: 1px solid $gray-300;
tr {
&:not(:first-child) > td[aria-colindex='1'] {
- border-top: 1px solid gray("300");
+ border-top: 1px solid $gray-300;
padding-top: 0.625rem;
}
@@ -126,29 +131,29 @@
&:before {
content: '';
- background-color: gray("200");
+ background-color: $gray-200;
width: 40%;
- border-right: 1px solid gray("300");
+ border-inline-end: 1px solid $gray-300;
}
&:after {
content: '';
- right: 0;
+ inset-inline-end: 0;
width: 60%;
}
&:nth-child(even)::after {
- background-color: gray("100"); // Zebra striping for the row
+ background-color: $gray-100; // Zebra striping for the row
}
}
td {
border: 0;
padding: 0.75rem;
- text-align: left !important;
+ text-align: start !important;
&:last-of-type {
- border-right: 0;
+ border-inline-end: 0;
}
}
}
@@ -156,12 +161,12 @@
.table.b-table.b-table-stacked-sm > tbody > tr > [data-label] {
&::before {
- text-align: left;
- padding-left: $spacer /2;
+ text-align: start;
+ padding-inline-start: calc($spacer / 2);
}
> div {
- padding-left: 1rem;
+ padding-inline-start: 1rem;
}
}
diff --git a/src/assets/styles/bmc/custom/_toasts.scss b/src/assets/styles/bmc/custom/_toasts.scss
index 4e2ad7f..900794f 100644
--- a/src/assets/styles/bmc/custom/_toasts.scss
+++ b/src/assets/styles/bmc/custom/_toasts.scss
@@ -1,16 +1,23 @@
-.b-toaster {
- top: 75px!important; // make sure toasts do not hide top header
+// Toast container positioning - below AppHeader
+.b-toaster,
+.toast-container {
+ top: calc(#{$header-height} + #{$spacer})!important; // position below AppHeader (48px + spacing)
}
// Toast component and status icon style
.toast {
- padding: $spacer/2 $spacer/2 $spacer/2 $spacer+2;
- border-width: 0 0 0 3px;
+ padding: calc(#{$spacer} / 2) calc(#{$spacer} / 2) calc(#{$spacer} / 2) $spacer+2;
+ border-width: 0 0 0 3px; // physical width retained; color via logical property
box-shadow: $box-shadow;
+ margin-bottom: $spacer; // vertical spacing between stacked toasts
.close {
font-weight: 300;
opacity: 1;
}
+ // Hide progress bar in all toasts (CSS workaround since JS props don't work as documented in Bootstrap Vue Next 0.40.8)
+ .progress {
+ display: none !important;
+ }
}
.toast-header {
@@ -19,15 +26,15 @@
background-color: inherit!important; //override specificity
border: none;
color: theme-color("dark")!important; //override specificity
- padding-bottom: 0;
+ padding-bottom: calc(#{$spacer} / 4); // spacing between header and body
}
.toast-icon {
display: flex;
- margin-right: 1rem;
+ margin-inline-end: 1rem;
svg {
- margin-left: -2.5rem;
+ margin-inline-start: -2.5rem;
}
+ .close {
@@ -37,25 +44,77 @@
.toast-body {
color: theme-color("dark");
- padding-top: 0;
+ padding-top: calc(#{$spacer} / 4); // spacing below header
+ white-space: pre-line; // Preserve newlines from \n characters
+ line-height: 1.6; // Better line spacing for multi-line content and timestamps
}
+// Bootstrap Vue 2 class names (backward compatibility)
.b-toast-success .toast {
- border-left-color: theme-color("success")!important;
+ border-inline-start-color: theme-color("success")!important;
background-color: theme-color-light("success")!important;
}
.b-toast-info .toast {
- border-left-color: theme-color("info")!important;
+ border-inline-start-color: theme-color("info")!important;
background-color: theme-color-light("info")!important;
}
.b-toast-danger .toast {
- border-left-color: theme-color("danger")!important;
+ border-inline-start-color: theme-color("danger")!important;
background-color: theme-color-light("danger")!important;
}
.b-toast-warning .toast {
- border-left-color: theme-color("warning")!important;
+ border-inline-start-color: theme-color("warning")!important;
+ background-color: theme-color-light("warning")!important;
+}
+
+// Bootstrap Vue Next class names
+.toast.bg-success {
+ border-inline-start-color: theme-color("success")!important;
+ background-color: theme-color-light("success")!important;
+}
+
+.toast.bg-info {
+ border-inline-start-color: theme-color("info")!important;
+ background-color: theme-color-light("info")!important;
+}
+
+.toast.bg-danger {
+ border-inline-start-color: theme-color("danger")!important;
+ background-color: theme-color-light("danger")!important;
+}
+
+.toast.bg-warning {
+ border-inline-start-color: theme-color("warning")!important;
+ background-color: theme-color-light("warning")!important;
+}
+
+// Override Bootstrap's solid variant backgrounds
+.toast.text-bg-success,
+.toast.text-bg-info,
+.toast.text-bg-danger,
+.toast.text-bg-warning {
+ color: theme-color("dark")!important;
+}
+
+.toast.text-bg-success {
+ border-inline-start-color: theme-color("success")!important;
+ background-color: theme-color-light("success")!important;
+}
+
+.toast.text-bg-info {
+ border-inline-start-color: theme-color("info")!important;
+ background-color: theme-color-light("info")!important;
+}
+
+.toast.text-bg-danger {
+ border-inline-start-color: theme-color("danger")!important;
+ background-color: theme-color-light("danger")!important;
+}
+
+.toast.text-bg-warning {
+ border-inline-start-color: theme-color("warning")!important;
background-color: theme-color-light("warning")!important;
}
\ No newline at end of file
diff --git a/src/assets/styles/bmc/helpers/_colors.scss b/src/assets/styles/bmc/helpers/_colors.scss
index cffe39d..648ede7 100644
--- a/src/assets/styles/bmc/helpers/_colors.scss
+++ b/src/assets/styles/bmc/helpers/_colors.scss
@@ -34,5 +34,17 @@
$success: $green;
$warning: $yellow;
+// Bootstrap 5 requires $theme-colors map
+$theme-colors: (
+ "danger": $danger,
+ "dark": $dark,
+ "info": $info,
+ "light": $light,
+ "primary": $primary,
+ "secondary": $secondary,
+ "success": $success,
+ "warning": $warning
+);
+
$loading-color: $primary;
$navbar-color: $dark;
diff --git a/src/assets/styles/bmc/helpers/_functions.scss b/src/assets/styles/bmc/helpers/_functions.scss
index 57956c9..0d89cc4 100644
--- a/src/assets/styles/bmc/helpers/_functions.scss
+++ b/src/assets/styles/bmc/helpers/_functions.scss
@@ -1,9 +1,32 @@
+@use "sass:math";
+
// This function is usually used to get a lighter
// theme variant color to use as a background color
@function theme-color-light($variant) {
- @return theme-color-level($variant, -11.3);
+ @return shift-color(map-get($theme-colors, $variant), -90.4%);
}
@function theme-color-dark($variant) {
- @return theme-color-level($variant, 2);
+ @return shift-color(map-get($theme-colors, $variant), 16%);
+}
+
+// Bootstrap 5 no longer provides theme-color(); define a compatible helper
+@function theme-color($variant) {
+ @return map-get($theme-colors, $variant);
+}
+
+// Bootstrap 4 compatibility: theme-color-level() function
+// Positive levels darken (mix with black), negative levels lighten (mix with white)
+// Each level is 8% change, matching Bootstrap 4 behavior
+@function theme-color-level($variant, $level: 0) {
+ $color: map-get($theme-colors, $variant);
+ $weight: $level * 8%;
+
+ @if $level > 0 {
+ @return shade-color($color, $weight);
+ } @else if $level < 0 {
+ @return tint-color($color, math.abs($weight));
+ } @else {
+ @return $color;
+ }
}
\ No newline at end of file
diff --git a/src/assets/styles/bootstrap/_index.scss b/src/assets/styles/bootstrap/_index.scss
index 847e83e..994a412 100644
--- a/src/assets/styles/bootstrap/_index.scss
+++ b/src/assets/styles/bootstrap/_index.scss
@@ -1,3 +1,18 @@
+// Import Bootstrap functions first
+@import "~bootstrap/scss/functions";
+
+// Import our custom colors and variables before Bootstrap variables
+@import "../bmc/helpers/colors";
+@import "../bmc/helpers/variables";
+
+// Import Bootstrap variables and maps
+@import "~bootstrap/scss/variables";
+@import "~bootstrap/scss/maps";
+@import "~bootstrap/scss/mixins";
+
+// Import our custom functions that depend on Bootstrap variables
+@import "../bmc/helpers/functions";
+
// Base
@import "~bootstrap/scss/root";
@import "~bootstrap/scss/reboot";
@@ -12,15 +27,11 @@
@import "~bootstrap/scss/buttons";
@import "~bootstrap/scss/card";
@import "~bootstrap/scss/close";
-@import "~bootstrap/scss/code";
-@import "~bootstrap/scss/custom-forms";
@import "~bootstrap/scss/dropdown";
@import "~bootstrap/scss/forms";
@import "~bootstrap/scss/grid";
@import "~bootstrap/scss/images";
-@import "~bootstrap/scss/input-group";
@import "~bootstrap/scss/list-group";
-@import "~bootstrap/scss/media";
@import "~bootstrap/scss/modal";
@import "~bootstrap/scss/nav";
@import "~bootstrap/scss/navbar";
@@ -33,5 +44,4 @@
@import "~bootstrap/scss/tooltip";
// Utils
-@import "~bootstrap/scss/utilities";
-@import "~bootstrap/scss/print";
\ No newline at end of file
+@import "~bootstrap/scss/utilities";
\ No newline at end of file