Procházet zdrojové kódy

chore(ui): Upgrade Tabler to v1.7.0

Compile Tabler through an explicit Sass module configuration and add
its --tblr- prefix pass to build.mjs. Pin most changed Tabler defaults
to keep NetBox's look, and use the combined navbar position. Fix the
Tom Select focus flash and spinner contrast, the page, tab and form bar
background, card table corners and the rack elevation background.

Fixes #23184
Martin Hauser před 4 dny
rodič
revize
7d20927d12

+ 1 - 1
docs/development/web-ui.md

@@ -41,7 +41,7 @@ NetBox generally follows the following guidelines for front-end code:
 - All TypeScript functions must have, at a minimum, a basic [JSDoc](https://jsdoc.app/) description of what the function is for and where it is used. If possible, document all function arguments via [`@param` JSDoc block tags](https://jsdoc.app/tags-param.html).
 - Expanding on NetBox's [dependency policy](style-guide.md#introducing-new-dependencies), new front-end dependencies should be avoided unless absolutely necessary. Every new front-end dependency adds to the CSS/JavaScript file size that must be loaded by the client and this should be minimized as much as possible. If adding a new dependency is unavoidable, use a tool like [Bundlephobia](https://bundlephobia.com/) to ensure the smallest possible library is used.
 - All UI elements must be usable on all common screen sizes, including mobile devices. Be sure to test newly implemented solutions (JavaScript included) on as many screen sizes and device types as possible.
-- NetBox aligns with Bootstrap's [supported Browsers and Devices](https://getbootstrap.com/docs/5.1/getting-started/browsers-devices/) list.
+- NetBox aligns with Tabler's [supported browsers](https://docs.tabler.io/ui/getting-started/browser-support) list.
 
 ## UI Development
 

+ 25 - 1
netbox/project-static/build.mjs

@@ -1,10 +1,12 @@
 // Compile NetBox's TypeScript and SCSS sources into dist/. Exits non-zero on any failure.
 import { rename, rm } from 'node:fs/promises';
 import { createRequire } from 'node:module';
-import { dirname } from 'node:path';
+import { basename, dirname } from 'node:path';
 import { fileURLToPath } from 'node:url';
 import * as esbuild from 'esbuild';
 import { sassPlugin } from 'esbuild-sass-plugin';
+import postcss from 'postcss';
+import prefixCustomProperties from 'postcss-prefix-custom-properties';
 
 // Resolve all paths relative to this directory, regardless of where the script is invoked from.
 // Both are needed: chdir() covers the file operations below, but esbuild resolves entry points
@@ -74,6 +76,22 @@ async function buildScripts() {
   ]);
 }
 
+// Tabler's build-time --tblr- prefix, with the ignore list of Tabler's own build
+const tablerPrefix = prefixCustomProperties({
+  prefix: 'tblr-',
+  ignore: [
+    /^--tblr-/,
+    /^--apx-/,
+    /^--bs-/,
+    /^--fc-/,
+    /^--gl-/,
+    /^--litepicker-/,
+    /^--plyr-/,
+    /^--ts-/,
+    '--section-bg',
+  ],
+});
+
 async function buildStyles() {
   await esbuild.build({
     ...common,
@@ -91,6 +109,12 @@ async function buildStyles() {
         quietDeps: true,
         // NetBox's own SCSS still relies on @import, which Dart Sass 3 will remove
         silenceDeprecations: ['import'],
+        // Only the main stylesheet compiles Tabler
+        async transform(css, resolveDir, filePath) {
+          if (basename(filePath) !== 'netbox.scss') return css;
+          const result = await postcss([tablerPrefix]).process(css, { from: filePath, map: false });
+          return result.css;
+        },
       }),
     ],
     loader: { '.woff2': 'file', '.svg': 'file' },

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 0 - 0
netbox/project-static/dist/netbox.css


+ 1 - 1
netbox/project-static/dist/rack_elevation.css

@@ -1 +1 @@
-svg{--nbx-rack-bg: var(--tblr-bg-surface-secondary);--nbx-rack-border: #000;--nbx-rack-slot-bg: #e9ecef;--nbx-rack-slot-border: #adb5bd;--nbx-rack-slot-hover-bg: #ced4da;--nbx-rack-link-color: #0d6efd;--nbx-rack-unit-color: #6c757d}svg[data-bs-theme=dark]{--nbx-rack-bg: rgb(27, 41, 58);--nbx-rack-border: #6c757d;--nbx-rack-slot-bg: #343a40;--nbx-rack-slot-border: #495057;--nbx-rack-slot-hover-bg: #212529;--nbx-rack-link-color: rgb(62.0392156863%, 77.2549019608%, 99.6862745098%);--nbx-rack-unit-color: #adb5bd}rect{box-sizing:border-box}text{text-anchor:middle;dominant-baseline:middle}svg{background-color:var(--nbx-rack-bg);font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Noto Sans,Liberation Sans,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-size:.875rem}svg .unit{margin:0;padding:5px 0;fill:var(--nbx-rack-unit-color)}svg .hidden{visibility:hidden}svg rect.shaded,svg image.shaded{opacity:25%}svg text.shaded{opacity:50%}svg .rack{fill:none;stroke-width:2px;stroke:var(--nbx-rack-border)}svg .slot{fill:var(--nbx-rack-slot-bg);stroke:var(--nbx-rack-slot-border)}svg .slot:hover{fill:var(--nbx-rack-slot-hover-bg)}svg .slot+.add-device{fill:var(--nbx-rack-link-color);opacity:0;pointer-events:none}svg .slot:hover+.add-device{opacity:1}svg .slot.occupied[class],svg .slot.occupied:hover[class]{fill:url(#occupied)}svg .slot.blocked[class],svg .slot.blocked:hover[class]{fill:url(#blocked)}svg .slot.blocked:hover+.add-device{opacity:0}svg .reservation[class]{fill:url(#reserved)}
+svg{--nbx-rack-bg: transparent;--nbx-rack-border: #000;--nbx-rack-slot-bg: #e9ecef;--nbx-rack-slot-border: #adb5bd;--nbx-rack-slot-hover-bg: #ced4da;--nbx-rack-link-color: #0d6efd;--nbx-rack-unit-color: #6c757d}svg[data-bs-theme=dark]{--nbx-rack-border: #6c757d;--nbx-rack-slot-bg: #343a40;--nbx-rack-slot-border: #495057;--nbx-rack-slot-hover-bg: #212529;--nbx-rack-link-color: rgb(62.0392156863%, 77.2549019608%, 99.6862745098%);--nbx-rack-unit-color: #adb5bd}rect{box-sizing:border-box}text{text-anchor:middle;dominant-baseline:middle}svg{background-color:var(--nbx-rack-bg);font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica Neue,Noto Sans,Liberation Sans,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-size:.875rem}svg .unit{margin:0;padding:5px 0;fill:var(--nbx-rack-unit-color)}svg .hidden{visibility:hidden}svg rect.shaded,svg image.shaded{opacity:25%}svg text.shaded{opacity:50%}svg .rack{fill:none;stroke-width:2px;stroke:var(--nbx-rack-border)}svg .slot{fill:var(--nbx-rack-slot-bg);stroke:var(--nbx-rack-slot-border)}svg .slot:hover{fill:var(--nbx-rack-slot-hover-bg)}svg .slot+.add-device{fill:var(--nbx-rack-link-color);opacity:0;pointer-events:none}svg .slot:hover+.add-device{opacity:1}svg .slot.occupied[class],svg .slot.occupied:hover[class]{fill:url(#occupied)}svg .slot.blocked[class],svg .slot.blocked:hover[class]{fill:url(#blocked)}svg .slot.blocked:hover+.add-device{opacity:0}svg .reservation[class]{fill:url(#reserved)}

+ 89 - 24
netbox/project-static/package-lock.json

@@ -12,7 +12,7 @@
         "@graphiql/toolkit": "0.12.1",
         "@mdi/font": "7.4.47",
         "@popperjs/core": "2.11.8",
-        "@tabler/core": "1.4.0",
+        "@tabler/core": "1.7.0",
         "bootstrap": "5.3.8",
         "flatpickr": "4.6.13",
         "graphiql": "5.4.0",
@@ -32,6 +32,8 @@
         "esbuild": "0.28.2",
         "esbuild-sass-plugin": "3.7.0",
         "eslint": "10.10.0",
+        "postcss": "8.5.30",
+        "postcss-prefix-custom-properties": "0.1.0",
         "prettier": "3.9.6",
         "sass": "1.104.1",
         "typescript": "6.0.3",
@@ -1994,13 +1996,12 @@
       }
     },
     "node_modules/@tabler/core": {
-      "version": "1.4.0",
-      "resolved": "https://registry.npmjs.org/@tabler/core/-/core-1.4.0.tgz",
-      "integrity": "sha512-5BigzOlbOH9N0Is4u0rYNRCiwtnUXWO57K9zwuscygcicAa8UV9MGaS4zTgQsZEtZ9tsNANhN/YD8gCBGKYCiw==",
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/@tabler/core/-/core-1.7.0.tgz",
+      "integrity": "sha512-f3a18Binkl4soRdzjaf7DhJFCuJXRJ45H1mJxmCFeHvqI2QIAZ6Djq2dVtYc1jJJgYYeIyVc+Rws8dK1yQp+Uw==",
       "license": "MIT",
       "dependencies": {
-        "@popperjs/core": "^2.11.8",
-        "bootstrap": "5.3.7"
+        "@popperjs/core": "^2.11.8"
       },
       "engines": {
         "node": ">=20"
@@ -2008,25 +2009,14 @@
       "funding": {
         "type": "github",
         "url": "https://github.com/sponsors/codecalm"
-      }
-    },
-    "node_modules/@tabler/core/node_modules/bootstrap": {
-      "version": "5.3.7",
-      "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.7.tgz",
-      "integrity": "sha512-7KgiD8UHjfcPBHEpDNg+zGz8L3LqR3GVwqZiBRFX04a1BCArZOz1r2kjly2HQ0WokqTO0v1nF+QAt8dsW4lKlw==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/twbs"
-        },
-        {
-          "type": "opencollective",
-          "url": "https://opencollective.com/bootstrap"
-        }
-      ],
-      "license": "MIT",
+      },
       "peerDependencies": {
-        "@popperjs/core": "^2.11.8"
+        "vanilla-calendar-pro": "^3.3.2"
+      },
+      "peerDependenciesMeta": {
+        "vanilla-calendar-pro": {
+          "optional": true
+        }
       }
     },
     "node_modules/@tanstack/react-virtual": {
@@ -3385,6 +3375,25 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/nanoid": {
+      "version": "3.3.20",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
+      "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
     "node_modules/natural-compare": {
       "version": "1.4.0",
       "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -3483,6 +3492,13 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/picomatch": {
       "version": "4.0.7",
       "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
@@ -3508,6 +3524,55 @@
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
+    "node_modules/postcss": {
+      "version": "8.5.30",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz",
+      "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.20",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.2"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/postcss-prefix-custom-properties": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/postcss-prefix-custom-properties/-/postcss-prefix-custom-properties-0.1.0.tgz",
+      "integrity": "sha512-sAKQSRw+KTl9G549Q9awV/ra06fOZNEIVIIRTwiSTbvkV1t8z7WPqNQ5XBbxF1fV2mp0yQFObavc5J/GxP0a6g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "postcss-value-parser": "^4.2.0"
+      },
+      "peerDependencies": {
+        "postcss": "^8.4.0"
+      }
+    },
+    "node_modules/postcss-value-parser": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+      "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/prelude-ls": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",

+ 3 - 1
netbox/project-static/package.json

@@ -23,7 +23,7 @@
     "@graphiql/toolkit": "0.12.1",
     "@mdi/font": "7.4.47",
     "@popperjs/core": "2.11.8",
-    "@tabler/core": "1.4.0",
+    "@tabler/core": "1.7.0",
     "bootstrap": "5.3.8",
     "flatpickr": "4.6.13",
     "graphiql": "5.4.0",
@@ -43,6 +43,8 @@
     "esbuild": "0.28.2",
     "esbuild-sass-plugin": "3.7.0",
     "eslint": "10.10.0",
+    "postcss": "8.5.30",
+    "postcss-prefix-custom-properties": "0.1.0",
     "prettier": "3.9.6",
     "sass": "1.104.1",
     "typescript": "6.0.3",

+ 77 - 0
netbox/project-static/styles/_tabler-config.scss

@@ -0,0 +1,77 @@
+// Tabler's Sass members configured for NetBox. Emits no CSS, so partials may @use it.
+@use 'variables' as netbox;
+
+@forward 'variables' show $rich-black, $rich-black-light, $rich-black-lighter, $rich-black-lightest,
+  $bright-teal, $dark-teal;
+@forward '@tabler/core/scss/config' with (
+  $font-family-sans-serif: netbox.$font-family-sans-serif,
+  $font-family-monospace: netbox.$font-family-monospace,
+  $sidebar-width: netbox.$sidebar-width,
+  $btn-padding-x: netbox.$btn-padding-x,
+  $btn-padding-y: netbox.$btn-padding-y,
+  $table-cell-padding-x: netbox.$table-cell-padding-x,
+  $table-cell-padding-y: netbox.$table-cell-padding-y,
+  $nav-pills-link-active-bg: netbox.$nav-pills-link-active-bg,
+  $form-check-input-border: netbox.$form-check-input-border,
+  $primary: netbox.$primary,
+  $navbar-active-border-color: netbox.$navbar-active-border-color,
+  // Tabler 1.4 values of defaults that Tabler 1.5 and 1.6 changed
+  $gray-50: #f9fafb,
+  $gray-100: #f3f4f6,
+  $gray-200: #e5e7eb,
+  $gray-300: #d1d5db,
+  $gray-400: #9ca3af,
+  $gray-500: #6b7280,
+  $gray-600: #4b5563,
+  $gray-700: #374151,
+  $gray-800: #1f2937,
+  $gray-900: #111827,
+  $gray-950: #030712,
+  $font-weight-bold: 600,
+  $form-text-color: light-dark(
+      color-mix(in oklab, var(--gray-800) 75%, transparent),
+      var(--secondary-color)
+    ),
+  $headings-color: inherit,
+  $navbar-dark-active-bg: rgba(255, 255, 255, 0.06),
+  $sidebar-inset: 0,
+  $sidebar-nav-padding-y: 0.5rem,
+  $sidebar-nav-padding-x: var(--page-padding),
+  $sidebar-nav-icon-width: 1.25rem,
+  $sidebar-brand-padding-x: 0,
+  // Tabler 1.6 values of defaults that Tabler 1.7 changed
+  $badge-bg-color: var(--bg-surface-secondary),
+  $btn-gradient: none,
+  $btn-highlight-color: transparent,
+  $btn-padding-x-sm: 0.5rem,
+  $btn-shade-color: transparent,
+  $btn-variant-border-color: transparent,
+  $code-bg: light-dark(var(--gray-100), var(--gray-900)),
+  $hover-bg: light-dark(
+      color-mix(in oklab, var(--body-color) 6%, transparent),
+      color-mix(in oklab, var(--body-color) 8%, transparent)
+    ),
+  $input-btn-padding-y-sm: 0.3125rem,
+  $input-group-addon-bg: var(--bg-surface-secondary),
+  $list-group-header-bg: var(--bg-surface-tertiary),
+  $progress-bg: var(--border-color),
+  // NetBox's own styling choices
+  $btn-border-radius-sm: var(--border-radius),
+  $dropdown-divider-margin-y: 0.25rem,
+  $enable-smooth-scroll: false,
+  $mark-padding: 0.1875em 0,
+  $navbar-light-active-bg: rgba(0, 0, 0, 0.06),
+  $pre-bg: transparent,
+  $pre-color: inherit,
+  $progress-height: 20px,
+  // Dark-mode subtle backgrounds at their Tabler 1.4 brightness, which 1.7's OKLab mix darkens
+  $success-bg-subtle-dark: color-mix(in oklab, var(--black) 65%, var(--success)),
+  $warning-bg-subtle-dark: color-mix(in oklab, var(--black) 65%, var(--warning)),
+  $danger-bg-subtle-dark: color-mix(in oklab, var(--black) 65%, var(--danger)),
+  $info-bg-subtle-dark: color-mix(in oklab, var(--black) 65%, var(--info)),
+  $secondary-bg-subtle-dark: color-mix(in oklab, var(--black) 70%, var(--secondary)),
+  $primary-bg-subtle-dark: color-mix(in oklab, var(--black) 75%, var(--primary))
+);
+
+// Tabler 1.5 dropped its Sass $prefix. NetBox partials still interpolate it.
+$prefix: 'tblr-';

+ 1 - 0
netbox/project-static/styles/custom/_code.scss

@@ -1,4 +1,5 @@
 @use 'sass:map';
+@use '../tabler-config' as *;
 
 // Serialized data from change records
 pre.change-data {

+ 8 - 6
netbox/project-static/styles/custom/_interfaces.scss

@@ -1,25 +1,27 @@
+@use 'sass:color';
 @use 'sass:map';
+@use '../tabler-config' as *;
 
 // Interface row coloring
 // Use --tblr-table-bg-state so type highlights take precedence over striping
 // (which sets --tblr-table-bg-type via the box-shadow cascade).
 tr[data-cable-status='connected'] {
-  --tblr-table-bg-state: #{rgba(map.get($theme-colors, 'green'), 0.15)};
+  --tblr-table-bg-state: #{color.change(map.get($theme-colors, 'green'), $alpha: 0.15)};
 }
 tr[data-cable-status='planned'] {
-  --tblr-table-bg-state: #{rgba(map.get($theme-colors, 'blue'), 0.15)};
+  --tblr-table-bg-state: #{color.change(map.get($theme-colors, 'blue'), $alpha: 0.15)};
 }
 tr[data-cable-status='decommissioning'] {
-  --tblr-table-bg-state: #{rgba(map.get($theme-colors, 'yellow'), 0.15)};
+  --tblr-table-bg-state: #{color.change(map.get($theme-colors, 'yellow'), $alpha: 0.15)};
 }
 tr[data-mark-connected='true'] {
-  --tblr-table-bg-state: #{rgba(map.get($theme-colors, 'success'), 0.15)};
+  --tblr-table-bg-state: #{color.change(map.get($theme-colors, 'success'), $alpha: 0.15)};
 }
 tr[data-virtual='true'] {
-  --tblr-table-bg-state: #{rgba(map.get($theme-colors, 'primary'), 0.15)};
+  --tblr-table-bg-state: #{color.change(map.get($theme-colors, 'primary'), $alpha: 0.15)};
 }
 tr[data-enabled='disabled'] {
-  --tblr-table-bg-state: #{rgba($gray-400, 0.15)};
+  --tblr-table-bg-state: #{color.change($gray-400, $alpha: 0.15)};
 }
 
 // Bootstrap paints --tblr-table-bg-state via an inset box-shadow on each cell.

+ 2 - 1
netbox/project-static/styles/custom/_misc.scss

@@ -1,4 +1,5 @@
 @use 'sass:map';
+@use '../tabler-config' as *;
 
 // Color labels
 span.color-label {
@@ -135,7 +136,7 @@ html[data-bs-theme='dark'] {
   margin-top: map.get($spacers, 4);
   padding: map.get($spacers, 3);
   padding-bottom: calc(#{map.get($spacers, 3)} + env(safe-area-inset-bottom, 0));
-  background: var(--#{$prefix}bg-surface-secondary);
+  background: var(--#{$prefix}page-bg);
   border-top: 1px solid var(--#{$prefix}border-color);
 
   &[data-sticky-when='always'],

+ 2 - 1
netbox/project-static/styles/custom/_notifications.scss

@@ -1,9 +1,10 @@
 @use 'sass:map';
+@use '../tabler-config' as *;
 
 // Mute read notifications
 tr[data-read='True'] {
   td {
     background-color: var(--#{$prefix}bg-surface-secondary);
-    color: $text-muted;
+    color: $text-secondary;
   }
 }

+ 5 - 4
netbox/project-static/styles/netbox.scss

@@ -1,9 +1,10 @@
-@import 'variables';
+@use 'tabler-config' as *;
 
 // Tabler & Tom-Select
-@import '@tabler/core/scss/core';
-@import 'tom-select/src/scss/tom-select.bootstrap5.scss';
-@import '@tabler/core/scss/vendor/tom-select';
+@use '@tabler/core/scss/tabler';
+// Tom Select's Sass source cannot take oklch colors. Tabler's adapter targets the compiled CSS.
+@use 'tom-select/dist/css/tom-select.bootstrap5' as tom-select-css;
+@use '@tabler/core/scss/vendor/tom-select';
 
 // Overrides of external libraries
 @import 'overrides/bootstrap';

+ 0 - 22
netbox/project-static/styles/overrides/_bootstrap.scss

@@ -1,14 +1,3 @@
-// Disable smooth scrolling for intra-page links
-html {
-  scroll-behavior: auto !important;
-}
-
-// Remove horizontal padding from highlighted text
-mark {
-  padding-left: 0;
-  padding-right: 0;
-}
-
 // Prevent dropdown menus from being clipped inside responsive tables
 .table-responsive {
   .dropdown,
@@ -23,17 +12,6 @@ mark {
   min-width: 80px;
 }
 
-// Reduce vertical margin around dropdown menu divider
-hr.dropdown-divider {
-  margin-bottom: 0.25rem;
-  margin-top: 0.25rem;
-}
-
-// Bootstrap forces the font weight for dropdown items to $font-weight-normal
-.dropdown-item {
-  font-weight: $font-weight-base;
-}
-
 // Restore support for old Bootstrap v3 colors
 .text-bg-black {
   @extend .text-bg-dark;

+ 31 - 35
netbox/project-static/styles/overrides/_tabler.scss

@@ -5,25 +5,24 @@
   font-variant-ligatures: none;
 }
 
-// TODO: Remove when Tabler releases fix for https://github.com/tabler/tabler/issues/2271
-// and NetBox upgrades to that version. Fix merged to Tabler dev branch in PR #2548.
+// Keep the layout from shifting when a page gains a scrollbar
 :root,
 :host {
   @include media-breakpoint-up(lg) {
-    margin-left: 0;
     scrollbar-gutter: stable;
   }
 }
 
-// Restore default foreground & background colors for <pre> blocks
-pre {
-  background-color: transparent;
-  color: inherit;
-}
-
 // Slightly increase alert contrast between bg and text
 .alert {
   background: var(--#{$prefix}bg-surface);
+  // Keep the gap Tabler 1.4 put between an alert's flex children
+  gap: 1rem;
+
+  // Tabler 1.6 colors alert text with the variant, 1.4 kept the body color
+  &:not(.alert-important) {
+    color: inherit;
+  }
 }
 
 // Permit copying of badge text
@@ -42,10 +41,6 @@ pre {
     outline: 2px solid var(--tblr-primary) !important;
   }
 }
-.btn-sm {
-  // $border-radius-sm (2px) is too small
-  border-radius: $border-radius;
-}
 
 // Dropdown items
 .dropdown-item {
@@ -66,16 +61,15 @@ pre {
   }
 }
 
+// Opaque page background, also painted by the active page tab and the sticky form actions
+:root {
+  --tblr-page-bg: color-mix(in srgb, var(--tblr-body-color) 2.5%, var(--tblr-bg-surface-secondary));
+}
+
 // Altering background colors
 .page,
 .page-tabs .nav-tabs .nav-link.active {
-  background-color: var(--#{$prefix}bg-surface-tertiary) !important;
-}
-
-// Navigation bar
-.navbar {
-  // Lighten highlight color of active item
-  --tblr-navbar-active-bg: rgba(0, 0, 0, 0.06);
+  background-color: var(--#{$prefix}page-bg) !important;
 }
 
 // Dark mode colors
@@ -83,6 +77,8 @@ pre {
   @include color-mode(dark, true) {
     --#{$prefix}alert-color: darken(var(--#{$prefix}warning), 10%);
     --#{$prefix}link-color: #{$bright-teal};
+    // Tabler 1.6 lightens the hovered link, 1.4 darkened it
+    --#{$prefix}link-hover-color: color-mix(in srgb, #{$bright-teal}, black 20%);
     --#{$prefix}link-color-rgb: 0, 242, 212;
     --#{$prefix}link-hover-color-rgb: 0, 242, 212;
     --#{$prefix}secondary: #{$gray-400};
@@ -90,6 +86,9 @@ pre {
     --#{$prefix}primary-fg: #{$rich-black};
     --#{$prefix}primary-rgb: 0, 242, 212;
     --#{$prefix}btn-active-color: #{$rich-black};
+    // Tabler 1.7's translucent tertiary surface hides card headers on the dark cards
+    --#{$prefix}bg-surface-tertiary: var(--#{$prefix}gray-800);
+    --#{$prefix}page-bg: #{$rich-black-light};
   }
 }
 
@@ -104,6 +103,16 @@ html[data-bs-theme='dark'] {
     color: $rich-black !important;
   }
 
+  // 4.x kept these gray-500 in dark mode, 1.7 paints them from the gray-400 secondary
+  .text-bg-secondary,
+  .text-bg-gray {
+    background-color: color-mix(
+      in oklab,
+      var(--tblr-gray-500) calc(var(--tblr-bg-opacity, 1) * 100%),
+      transparent
+    ) !important;
+  }
+
   // Change content color when primary teal changes with theme
   .bg-primary {
     .card-title,
@@ -127,10 +136,6 @@ html[data-bs-theme='dark'] {
   .page-header {
     background-color: $rich-black;
   }
-  .page,
-  .page-tabs .nav-tabs .nav-link.active {
-    background-color: $rich-black-light !important;
-  }
 
   .page-link.active,
   .active > .page-link {
@@ -161,21 +166,12 @@ html[data-bs-theme='dark'] {
     --tblr-table-hover-color: inherit;
   }
 
-  // Tabler bakes a white glyph into the checked data URI, which reads at 1.4:1 against
-  // the teal dark-mode primary. A data URI is an isolated document, so the color cannot
-  // come from a custom property and has to be baked in at build time.
+  // Tabler's white checkbox glyph reads at 1.4:1 against the teal dark-mode primary
   .form-check-input:checked[type='checkbox'] {
-    --tblr-form-check-bg-image: #{escape-svg(
-        url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='16' height='16'><path fill='none' stroke='#{$rich-black}' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8.5l2.5 2.5l5.5 -5.5'/></svg>")
-      )};
+    --tblr-form-check-icon-color: #{$rich-black};
   }
 }
 
-// Do not apply padding to <code> elements inside a <pre>
-pre code {
-  padding: unset;
-}
-
 // Use an icon instead of Tabler's native "caret" for dropdowns (avoids a Safari bug)
 .dropdown-toggle:after {
   font-family: 'Material Design Icons';

+ 47 - 0
netbox/project-static/styles/overrides/_tomselect.scss

@@ -18,3 +18,50 @@ select.tomselected.is-invalid + div.ts-wrapper {
 select.tomselected[aria-invalid='true']:not(.is-invalid) + div.ts-wrapper {
   border: 1px solid $red;
 }
+
+// Tom Select's compiled preset reads Bootstrap tokens that Tabler only defines with its own prefix.
+.ts-wrapper,
+.ts-dropdown {
+  --bs-body-bg: var(--tblr-bg-forms);
+  --bs-body-color: var(--tblr-body-color);
+  --bs-border-color: var(--tblr-border-color);
+  --bs-border-color-translucent: var(--tblr-border-color-translucent);
+  --bs-border-radius: var(--tblr-border-radius);
+  --bs-border-radius-lg: var(--tblr-border-radius-lg);
+  --bs-border-radius-sm: var(--tblr-border-radius-sm);
+  --bs-border-width: var(--tblr-border-width);
+}
+
+// The compiled preset sizes the wrapper and colors the active item with Bootstrap's defaults.
+.ts-wrapper {
+  min-height: $input-height;
+}
+.input-group-sm > .ts-wrapper,
+.ts-wrapper.form-select-sm,
+.ts-wrapper.form-control-sm {
+  min-height: $input-height-sm;
+}
+.input-group-lg > .ts-wrapper,
+.ts-wrapper.form-select-lg,
+.ts-wrapper.form-control-lg {
+  min-height: $input-height-lg;
+}
+.ts-wrapper.multi .ts-control > div.active {
+  background: $primary;
+}
+
+// Tabler sets the transparent start of its focus ring fade only on the focused control
+.ts-control {
+  outline-color: transparent;
+}
+
+// Tom Select's fixed spinner gray is too faint on light surfaces
+.ts-dropdown .spinner::after {
+  border-color: light-dark(var(--tblr-gray-500), var(--tblr-gray-300)) transparent;
+}
+
+// API selects wrap Tom Select in a .d-flex, out of reach of the input group's corner rules
+.input-group > .d-flex:not(:first-child) > .ts-wrapper {
+  border-top-left-radius: 0;
+  border-bottom-left-radius: 0;
+}

+ 3 - 2
netbox/project-static/styles/svg/rack_elevation.scss

@@ -7,8 +7,10 @@
  */
 
 svg {
+  // Transparent in both modes, so the elevation shows the page or card behind it
+  --nbx-rack-bg: transparent;
+
   // Light mode values
-  --nbx-rack-bg: var(--tblr-bg-surface-secondary);
   --nbx-rack-border: #{$black};
   --nbx-rack-slot-bg: #{$gray-200};
   --nbx-rack-slot-border: #{$gray-500};
@@ -18,7 +20,6 @@ svg {
 
   &[data-bs-theme='dark'] {
     // Dark mode values
-    --nbx-rack-bg: rgb(27, 41, 58);
     --nbx-rack-border: #{$gray-600};
     --nbx-rack-slot-bg: #{$gray-800};
     --nbx-rack-slot-border: #{$gray-700};

+ 10 - 0
netbox/project-static/styles/transitional/_cards.scss

@@ -31,5 +31,15 @@
     .table {
       margin-bottom: 0;
     }
+
+    // Clip the scrolling table to the card corners, which its header and row fills would cover
+    > .table-responsive:first-child {
+      border-top-left-radius: var(--#{$prefix}card-inner-border-radius);
+      border-top-right-radius: var(--#{$prefix}card-inner-border-radius);
+    }
+    > .table-responsive:last-child {
+      border-bottom-left-radius: var(--#{$prefix}card-inner-border-radius);
+      border-bottom-right-radius: var(--#{$prefix}card-inner-border-radius);
+    }
   }
 }

+ 0 - 3
netbox/project-static/styles/transitional/_layout.scss

@@ -1,7 +1,4 @@
 // Tweak layout section colors
-.page {
-  background-color: var(--#{$prefix}bg-surface-secondary);
-}
 .page-header {
   background-color: var(--#{$prefix}bg-surface);
   min-height: 0;

+ 36 - 3
netbox/project-static/styles/transitional/_navigation.scss

@@ -1,10 +1,30 @@
 // Navbar and light theme styling
 .navbar-vertical.navbar-expand-lg {
-  // Tabler forces `overflow-y: scroll` on the vertical navbar, which renders a
-  // permanent scrollbar in Chromium browsers even when scrolling isn't needed.
-  // Use `auto` so the scrollbar only appears when the menu overflows.
+  // Scroll the whole sidebar as Tabler 1.4 did. sidenav.ts saves and restores its offset.
   @include media-breakpoint-up(lg) {
     overflow-y: auto;
+
+    > [class^='container'] {
+      height: auto;
+      min-height: 100%;
+    }
+    .navbar-collapse {
+      min-height: auto;
+      overflow-y: visible;
+    }
+    .navbar-brand {
+      justify-content: center;
+    }
+
+    // Tabler 1.6 drops the bar that marks the active menu group
+    .navbar-collapse .nav-item.active:after {
+      content: '';
+      border-bottom-width: 0;
+      border-left-width: 3px;
+      inset-inline-end: auto;
+      top: 0;
+      bottom: 0;
+    }
   }
 
   // Gap for the reveal in sidenav.ts, which scrolls with `block: 'nearest'`
@@ -34,6 +54,8 @@
     .dropdown-menu {
       // Adjust hover color & style for menu items
       .dropdown-item {
+        border-radius: 0;
+
         > a {
           color: $rich-black;
         }
@@ -53,6 +75,12 @@
             visibility: visible;
           }
         }
+
+        // Tabler 1.6 sets the current item in medium weight
+        &.active,
+        &:active {
+          font-weight: inherit;
+        }
       }
     }
   }
@@ -62,6 +90,11 @@
     z-index: 1;
   }
 
+  // Tabler 1.6 sets the active menu group in medium weight
+  .navbar-nav .nav-item.active > .nav-link {
+    font-weight: inherit;
+  }
+
   // Logo text for non-community editions
   .navbar-brand {
     // Reduce logo padding on mobile view

+ 0 - 2
netbox/project-static/styles/transitional/_progress.scss

@@ -1,6 +1,4 @@
 .progress {
-  height: 20px;
-
   .progress-label {
     display: flex;
     flex-direction: column;

+ 1 - 0
netbox/templates/base/base.html

@@ -7,6 +7,7 @@
 <!DOCTYPE html>
 <html
   lang="en"
+  data-bs-navbar-position="combined"
   data-netbox-url-name="{{ request.resolver_match.url_name }}"
   data-netbox-version="{{ settings.VERSION }}"
   {% if request.user.is_authenticated %}

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů