From 02ac3f525908f26186bb2bee9e8bb7ed8dc225b8 Mon Sep 17 00:00:00 2001 From: Cell <1024@lruihao.cn> Date: Thu, 12 Mar 2026 17:28:15 +0800 Subject: [PATCH] feat(code): add shadow effect for code blocks (#713) --- apps/test/content/posts/code-tabs-test.md | 43 +++++++++++++++++++ apps/test/content/posts/codeblock-test.md | 32 ++++++++++++++ assets/css/_core/_mixin/_box-shadow.scss | 26 +++++++++++ assets/css/_core/_mixin/_index.scss | 3 +- assets/css/_core/_root.scss | 3 ++ assets/css/_page/_media.scss | 6 ++- assets/css/_page/_single/_code.scss | 25 +++++++++-- .../_single/_shortcodes/_admonition.scss | 3 +- assets/js/theme.js | 12 +++++- hugo.toml | 2 + .../_partials/plugin/code-block-wrapper.html | 20 ++++++--- 11 files changed, 160 insertions(+), 15 deletions(-) create mode 100644 assets/css/_core/_mixin/_box-shadow.scss diff --git a/apps/test/content/posts/code-tabs-test.md b/apps/test/content/posts/code-tabs-test.md index 943ed2ee..86428d54 100644 --- a/apps/test/content/posts/code-tabs-test.md +++ b/apps/test/content/posts/code-tabs-test.md @@ -53,6 +53,25 @@ def greet(name): return f"Hello, {name}!" ``` +## Shadow + +```js {group=tab-shadow name="Always" shadow="always"} +function greet(name) { + return `Hello, ${name}!` +} +``` + +```python {group=tab-shadow name="Hover" shadow="hover"} +def greet(name): + return f"Hello, {name}!" +``` + +```go {group=tab-shadow name="Never" shadow="never"} +func greet(name string) string { + return fmt.Sprintf("Hello, %s!", name) +} +``` + ## Mode ```ts {group=tab-mode name="Classic"} @@ -121,3 +140,27 @@ function shortFunction() { console.log('This is a short function.') } ``` + +## Code tabs inside other blocks + +> [!NOTE]+ +> +> ```python {group=tab-nested} +> print("Python") +> ``` +> +> ```go {group=tab-nested} +> fmt.Println("Go") +> ``` + +{{< details open=true >}} + +```python {group=tab2} +print("Python") +``` + +```go {group=tab2} +fmt.Println("Go") +``` + +{{< /details >}} diff --git a/apps/test/content/posts/codeblock-test.md b/apps/test/content/posts/codeblock-test.md index ba4a9a94..c76a10bc 100644 --- a/apps/test/content/posts/codeblock-test.md +++ b/apps/test/content/posts/codeblock-test.md @@ -64,6 +64,38 @@ hello('FixIt') // Hello, FixIt! ``` +## Shadow + +{{< tabs >}} +{{% tab title="Always" %}} + +```js {shadow="always"} +function hello(x = 'world') { + console.log(`Hello, ${x}!`) +} +``` + +{{% /tab %}} +{{% tab title="Hover" %}} + +```js {shadow="hover"} +function hello(x = 'world') { + console.log(`Hello, ${x}!`) +} +``` + +{{% /tab %}} +{{% tab title="Never" %}} + +```js {shadow="never"} +function hello(x = 'world') { + console.log(`Hello, ${x}!`) +} +``` + +{{% /tab %}} +{{< /tabs >}} + ## Collapsed/Expanded ``` diff --git a/assets/css/_core/_mixin/_box-shadow.scss b/assets/css/_core/_mixin/_box-shadow.scss new file mode 100644 index 00000000..e630e647 --- /dev/null +++ b/assets/css/_core/_mixin/_box-shadow.scss @@ -0,0 +1,26 @@ +/// Mixin to apply box-shadow with timing control +/// @param {String} $when - When to show shadow: always | hover | never (default: always) +/// @param {List} $shadow - The box-shadow value +/// @example +/// @include box-shadow; +/// @include box-shadow(hover); +/// @include box-shadow(never); +/// @include box-shadow(always, 0 4px 8px rgba(0, 0, 0, 0.15)); +@mixin box-shadow( + $when: always, + $shadow: (0 1px 2px -2px rgba(0, 0, 0, 0.08), 0 3px 6px 0 rgba(0, 0, 0, 0.06), 0 5px 12px 4px rgba(0, 0, 0, 0.04)) +) { + @if $when == always { + box-shadow: $shadow; + } @else if $when == hover { + transition: box-shadow 0.3s fixit-var(bezier); + + &:hover { + box-shadow: $shadow; + } + } @else if $when == never { + box-shadow: none; + } @else { + @warn "box-shadow mixin: unknown $when value '#{$when}'. Expected always, hover, or never."; + } +} diff --git a/assets/css/_core/_mixin/_index.scss b/assets/css/_core/_mixin/_index.scss index 46e6e920..3c231157 100644 --- a/assets/css/_core/_mixin/_index.scss +++ b/assets/css/_core/_mixin/_index.scss @@ -1,8 +1,9 @@ -@import 'theme-vars'; @import 'blur'; @import 'border-radius'; +@import 'box-shadow'; @import 'compatibility'; @import 'link'; @import 'loading'; @import 'scrollbar-width'; +@import 'theme-vars'; @import 'z-index'; diff --git a/assets/css/_core/_root.scss b/assets/css/_core/_root.scss index d44f1ac5..f1bc7814 100644 --- a/assets/css/_core/_root.scss +++ b/assets/css/_core/_root.scss @@ -55,6 +55,9 @@ scrollbar-width: thin, scrollbar-width-legacy: 12px, + // Animations + bezier: cubic-bezier(.4, 0, .2, 1), + // Other utilities breadcrumb-height: 0px, divider-edge-weak: linear-gradient(to right, transparent, fixit-var(divider-bg, #d0d0d5), transparent), diff --git a/assets/css/_page/_media.scss b/assets/css/_page/_media.scss index cee39090..6dc597cc 100644 --- a/assets/css/_page/_media.scss +++ b/assets/css/_page/_media.scss @@ -154,7 +154,8 @@ } } [data-header-desktop='sticky'] { - .page .content .highlight.code-block .code-header { + .page .content .highlight.code-block .code-header, + .page .content .code-tabs .tabs-header { top: calc(#{fixit-var(header-height)} + #{fixit-var(breadcrumb-height)}); } } @@ -193,7 +194,8 @@ } } [data-header-mobile='sticky'] { - .page .content .highlight.code-block .code-header { + .page .content .highlight.code-block .code-header, + .page .content .code-tabs .tabs-header { top: calc(#{fixit-var(header-height)} + #{fixit-var(breadcrumb-height)}); } } diff --git a/assets/css/_page/_single/_code.scss b/assets/css/_page/_single/_code.scss index 853a18f4..7b4f7172 100644 --- a/assets/css/_page/_single/_code.scss +++ b/assets/css/_page/_single/_code.scss @@ -132,6 +132,14 @@ pre { // code fences blocks wrapped &.code-block { + &[data-shadow='always'] { + @include box-shadow(always); + } + + &[data-shadow='hover'] { + @include box-shadow(hover); + } + &.instant-height .code-wrapper { transition: height 0s !important; } @@ -267,10 +275,7 @@ pre { &.is-fullscreen { position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; + inset: 0; margin: 0 !important; overflow: auto; @include z-index(fixed, 1); @@ -567,6 +572,14 @@ pre { border: 1px solid fixit-var(global-border-color); @include border-radius; + &[data-shadow='always'] { + @include box-shadow(always); + } + + &[data-shadow='hover'] { + @include box-shadow(hover); + } + &:has(.code-block.active .code-expand-btn) { margin-bottom: 1rem; } @@ -585,6 +598,9 @@ pre { font-size: fixit-var(code-block-font-size); color: fixit-var(code-header-color); background-color: fixit-var(code-header-background-color); + position: sticky; + top: fixit-var(breadcrumb-height); + @include z-index(base); @include border-radius(top); .tabs-items { @@ -662,6 +678,7 @@ pre { display: none; margin: 0; @include border-radius(bottom); + @include box-shadow(never); &.active { display: block; diff --git a/assets/css/_page/_single/_shortcodes/_admonition.scss b/assets/css/_page/_single/_shortcodes/_admonition.scss index 2ee501d3..be1ff44a 100644 --- a/assets/css/_page/_single/_shortcodes/_admonition.scss +++ b/assets/css/_page/_single/_shortcodes/_admonition.scss @@ -60,7 +60,8 @@ margin: 0; } - .code-block .code-header { + .code-block .code-header, + .code-tabs .tabs-header { top: initial !important; } } diff --git a/assets/js/theme.js b/assets/js/theme.js index 6b676e33..aba12c54 100644 --- a/assets/js/theme.js +++ b/assets/js/theme.js @@ -639,7 +639,7 @@ class FixIt { // insert container before the first block const $firstBlock = $tabs[0]; $firstBlock.parentNode.insertBefore($container, $firstBlock); - + const activeTabIndex = $tabs.findIndex(tab => tab.classList.contains('active')); $tabs.forEach(($tab, index) => { const title = $tab.dataset.tabTitle || 'Code'; @@ -671,7 +671,15 @@ class FixIt { $tabs.forEach(b => b.classList.remove('active')); $tab.classList.add('active'); - // 4. move new buttons to actions + // 4. sync shadow mode data attribute + const shadowMode = $tab?.dataset.shadow; + if (shadowMode) { + $container.dataset.shadow = shadowMode; + } else { + delete $container.dataset.shadow; + } + + // 5. move new buttons to actions const $codeHeader = $tab.querySelector('.code-header'); if ($codeHeader) { $codeHeader.querySelectorAll('.action-btn').forEach(btn => $actions.appendChild(btn)); diff --git a/hugo.toml b/hugo.toml index 2af36e65..a523b12e 100644 --- a/hugo.toml +++ b/hugo.toml @@ -1144,6 +1144,8 @@ mode = "classic" wrapperClass = "" # the maximum number of lines to show in the code block preview maxShownLines = 10 +# whether to show shadow effect for code blocks, available values: ["always", "hover", "never"] +shadow = "never" # whether to enable code copy button copyable = true # [classic] whether to enable code download button in the code block header diff --git a/layouts/_partials/plugin/code-block-wrapper.html b/layouts/_partials/plugin/code-block-wrapper.html index dc3aa35b..488ba03e 100644 --- a/layouts/_partials/plugin/code-block-wrapper.html +++ b/layouts/_partials/plugin/code-block-wrapper.html @@ -58,11 +58,25 @@ (printf `class="%s" data-copyable="%v"` $class $config.copyable) -}} {{- end -}} + {{- if ne $config.shadow "never" -}} + {{- $wrapper = + replace $wrapper + (printf `class="%s"` $class) + (printf `class="%s" data-shadow="%s"` $class $config.shadow) + -}} + {{- end -}} + {{- if $config.group -}} + {{- $tabTitle := $config.name | default (strings.FirstUpper .Type) | default "Code" -}} + {{- $wrapper = + replace $wrapper + (printf `class="%s"` $class) + (printf `class="%s" data-tab-group="%s" data-tab-title="%s"` $class $config.group $tabTitle) -}} + {{- end -}} {{- if ne .Options.linenostart nil -}} {{- $wrapper = replace $wrapper `class="code-wrapper"` - (printf `class="code-wrapper" data-line-start="%v"` .) + (printf `class="code-wrapper" data-line-start="%v"` .Options.linenostart) -}} {{- end -}} {{- $wrapperStyle := printf "--fi-max-shown-lines:%v;--fi-line-digit:%v;--fi-line-start:%v;" $maxShownLines $lineNosDigit $lineNoStart -}} @@ -80,10 +94,6 @@ $lineNosDigit ) -}} - {{- if $config.group -}} - {{- $tabTitle := $config.name | default (strings.FirstUpper .Type) | default "Code" -}} - {{- $wrapper = replace $wrapper (printf `class="%s"` $class) (printf `class="%s" data-tab-group="%s" data-tab-title="%s"` $class $config.group $tabTitle) -}} - {{- end -}} {{- $wrapper = replace $wrapper (printf `class="%s"` $class)