hexo-theme-stellar 1.43.0 → 1.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.44.0
4
+
5
+ > 发布日期:2026-08-21
6
+
7
+ ### 新功能
8
+ - 新增可选的卡片 Spotlight 光斑与 3D Tilt 倾斜效果,可通过 `plugins.card_hover` 配置启用、光斑颜色和最大倾斜角度,并按组件独立接入与降级
9
+ - Wiki Hero 操作按钮接入 Spotlight 光效,背景图片与动画改为独立配置,Galaxy 支持逐实例参数覆盖、图片叠加和 WebGL 失败降级
10
+ - 桌面端新增 `Command+K` / `Ctrl+K` 聚焦搜索框快捷键,在窄屏、编辑区域和输入法组合状态下保留原生行为
11
+
12
+ ### 样式与重构
13
+ - 搜索控件改用原生搜索输入框、搜索地标和标签关联,完善无障碍语义;本地搜索与 Algolia 结果统一可点击区域、表面样式和 Spotlight 生命周期
14
+ - 笔记标签行复用文章标签胶囊的 hashtag 图标、间距与交互样式
15
+
16
+ ### 升级注意(配置变更与破坏性改动)
17
+ - Wiki Hero 的 Galaxy 配置由 `background: galaxy` 迁移为 `animation.type: galaxy`,参数放在 `animation.params`;`background` 现在仅用于背景图片,并可与动画同时显示
18
+ - `plugins.card_hover.enable` 默认为 `false`,升级后不会自动启用卡片动态效果
19
+
20
+ Full Changelog: [1.43.1...1.44.0](https://github.com/xaoxuu/hexo-theme-stellar/compare/1.43.1...1.44.0)
21
+
22
+ ## 1.43.1
23
+
24
+ > 发布日期:2026-08-20
25
+
26
+ ### 修复
27
+ - 左栏 Footer Social 的外部图片图标统一为 24×24px,非方形图片保持原始比例,避免固有尺寸撑宽按钮或被裁切
28
+ - 发版脚本自动同步主题配置、包信息与安装知识库的版本号,并在最终待提交状态执行完整质量检查
29
+
30
+ Full Changelog: [1.43.0...1.43.1](https://github.com/xaoxuu/hexo-theme-stellar/compare/1.43.0...1.43.1)
31
+
3
32
  ## 1.43.0
4
33
 
5
34
  > 发布日期:2026-08-20
@@ -2,7 +2,8 @@
2
2
 
3
3
  ## React Bits
4
4
 
5
- The Wiki Galaxy WebGL background is adapted from the Galaxy component in
5
+ The Wiki Galaxy WebGL background and Card Hover spotlight layer are adapted
6
+ from the Galaxy and Spotlight Card components in
6
7
  [React Bits](https://github.com/DavidHDev/react-bits).
7
8
 
8
9
  MIT License
package/_config.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  ######## Stellar info ########
2
2
  stellar:
3
- version: '1.43.0'
3
+ version: '1.44.0'
4
4
  homepage: 'https://xaoxuu.com/wiki/stellar/'
5
5
  repo: 'https://github.com/xaoxuu/hexo-theme-stellar'
6
6
  main_css: /css/main.css
@@ -686,6 +686,12 @@ plugins:
686
686
  # 页面存在 [data-text-adaptive] 元素时才按需加载(theme:背景图平均色 lighten/darken;contrast:黑白对比)
687
687
  adaptive_text:
688
688
  enable: true
689
+
690
+ # 卡片鼠标跟随光斑与 3D 倾斜:通过 card-hover 组合类按组件接入
691
+ card_hover:
692
+ enable: false
693
+ spotlight_color: 'rgba(255, 255, 255, 0.25)'
694
+ max_tilt: 3 # 最大倾斜角度(deg),运行时限制在 0~8
689
695
 
690
696
  # 赫蹏 (Heti) - 专为中文网页内容设计的排版样式增强
691
697
  # https://github.com/sivan/heti
@@ -5,7 +5,7 @@ var entryPrefix = entry.prefix == null ? '' : String(entry.prefix)
5
5
  var entryDescription = entry.description == null ? '' : String(entry.description)
6
6
  var entryMeta = entry.meta == null ? '' : String(entry.meta)
7
7
  var entryClassName = typeof entry.className === 'string' ? entry.className.trim() : ''
8
- var entryClasses = 'ui-collection__item'
8
+ var entryClasses = 'ui-collection__item card-hover card-hover--spotlight'
9
9
  if (entry.active) {
10
10
  entryClasses += ' is-active'
11
11
  }
@@ -9,7 +9,8 @@ function layoutWikiCover() {
9
9
  return el;
10
10
  }
11
11
 
12
- var background = proj.background;
12
+ var background = typeof proj.background === 'string' && proj.background.length > 0 ? proj.background : '';
13
+ var animation = proj.animation && typeof proj.animation === 'object' ? proj.animation : {};
13
14
  var preview = proj.preview || {};
14
15
  var commands = Array.isArray(preview.commands) ? preview.commands.filter(function (item) {
15
16
  return item && item.codes;
@@ -21,8 +22,10 @@ function layoutWikiCover() {
21
22
  var actions = Array.isArray(proj.actions) ? proj.actions.filter(function (item) {
22
23
  return item && item.title && item.url;
23
24
  }) : [];
24
- var isGalaxy = background === 'galaxy';
25
- var isImageBackground = background && !isGalaxy;
25
+ var isGalaxy = animation.type === 'galaxy';
26
+ var isImageBackground = background.length > 0;
27
+ var galaxyParams = animation.params && typeof animation.params === 'object' && !Array.isArray(animation.params) ? animation.params : {};
28
+ var galaxyParamsJson = escape_html(JSON.stringify(galaxyParams));
26
29
  var projectName = proj.name || proj.title || page.wiki;
27
30
  var adaptiveTextAttr = '';
28
31
  if (isImageBackground) {
@@ -33,12 +36,13 @@ function layoutWikiCover() {
33
36
 
34
37
  el += '<div class="l_cover wiki wiki-hero' + (hasPreview ? '' : ' without-preview') + '">';
35
38
  el += '<div class="wiki-cover-background' + (isGalaxy ? ' galaxy' : '') + (isImageBackground ? ' image' : '') + '" aria-hidden="true">';
36
- if (isGalaxy) {
37
- el += '<canvas></canvas>';
38
- } else if (isImageBackground) {
39
+ if (isImageBackground) {
39
40
  el += '<img class="wiki-cover-background-image" src="' + escape_html(background) + '" alt="" />';
40
41
  el += '<img class="wiki-cover-background-blur" src="' + escape_html(background) + '" alt="" />';
41
42
  }
43
+ if (isGalaxy) {
44
+ el += '<canvas data-galaxy-params="' + galaxyParamsJson + '"></canvas>';
45
+ }
42
46
  el += '<div class="wiki-cover-background-overlay"></div></div>';
43
47
  el += '<div class="wiki-cover-layout"' + adaptiveTextAttr + '>';
44
48
  el += '<nav class="wiki-navbar" aria-label="' + escape_html(config.title) + '">';
@@ -58,11 +62,11 @@ function layoutWikiCover() {
58
62
  }
59
63
  el += '<div class="wiki-cover-actions">';
60
64
  if (repo) {
61
- el += '<a class="button wiki-cover-source" href="https://github.com/' + escape_html(repo) + '" target="_blank" rel="external nofollow noopener noreferrer">' + icon('default:github') + '<span>' + escape_html(__('btn.source')) + '</span></a>';
65
+ el += '<a class="button wiki-cover-source card-hover card-hover--spotlight" href="https://github.com/' + escape_html(repo) + '" target="_blank" rel="external nofollow noopener noreferrer">' + icon('default:github') + '<span>' + escape_html(__('btn.source')) + '</span></a>';
62
66
  }
63
- el += '<a class="button" href="#start">' + icon('default:documents') + '<span>' + escape_html(__('btn.docs')) + '</span></a>';
67
+ el += '<a class="button card-hover card-hover--spotlight" href="#start">' + icon('default:documents') + '<span>' + escape_html(__('btn.docs')) + '</span></a>';
64
68
  actions.forEach(function (item) {
65
- el += '<a class="button" href="' + escape_html(item.url) + '" target="_blank" rel="external nofollow noopener noreferrer">';
69
+ el += '<a class="button card-hover card-hover--spotlight" href="' + escape_html(item.url) + '" target="_blank" rel="external nofollow noopener noreferrer">';
66
70
  if (item.icon) {
67
71
  el += icon(item.icon);
68
72
  }
@@ -48,7 +48,7 @@ function layoutDiv() {
48
48
  el += '</summary>'
49
49
  el += '<div class="dropdown-menu ui-collection" data-ui-surface="glass" data-layout="list" data-variant="nav" data-density="compact">'
50
50
  for (let child of items) {
51
- el += '<a class="dropdown-item ui-collection__item"' + linkAttrs(child) + '>'
51
+ el += '<a class="dropdown-item ui-collection__item card-hover card-hover--spotlight"' + linkAttrs(child) + '>'
52
52
  if (child.icon) {
53
53
  el += '<span class="ui-collection__leading">' + icon(child.icon, 'no-lazy', true) + '</span>'
54
54
  }
@@ -1,9 +1,9 @@
1
- <%# 笔记的所属标签 %>
1
+ <%# 笔记的所属标签(容器与胶囊样式复用 article/article_tags.ejs 的 article-tags) %>
2
2
  <% if (page.tags) { %>
3
- <div class="tag-list">
3
+ <div class="article-tags<%= scrollreveal(' ') %>">
4
4
  <% for (const t of page.tags) { %>
5
5
  <% const tag = notebook.tagTree.get(t.toLowerCase()) %>
6
- <a class="tag" href="<%= pretty_url(tag.path) %>"><%= tag.name %></a>
6
+ <a class="tag" href="<%= pretty_url(tag.path) %>"><%- icon('default:hashtag') %><%- escape_html(tag.name) %></a>
7
7
  <% } %>
8
8
  </div>
9
9
  <% } %>
@@ -119,7 +119,7 @@ if (pinItems.length > 0) {
119
119
  var groupKey = pinType == 'wiki' ? 'wiki' : 'post'
120
120
  var sliderInterval = 5000 // 自动轮播间隔固定 5000ms
121
121
  var el = ''
122
- el += '<div class="pin-slider" id="pin-slider" data-interval="' + sliderInterval + '" data-key="' + pinType + '" data-group="' + groupKey + '">'
122
+ el += '<div class="pin-slider card-hover card-hover--spotlight card-hover--tilt" id="pin-slider" data-interval="' + sliderInterval + '" data-key="' + pinType + '" data-group="' + groupKey + '">'
123
123
  el += '<div class="pin-slider-track">'
124
124
  el += slidesHtml
125
125
  el += '</div>'
@@ -7,7 +7,7 @@ function layoutDiv() {
7
7
  var bg = background || theme.default.topic;
8
8
  // 注意:不能用 `var href` 承接,var 提升会遮蔽 partial 传入的 href 参数
9
9
  var link = pretty_url(href || '/');
10
- el += '<a class="cover" position="bottom" style="--cover-url:url(\'' + escape_html(bg.replace(/'/g, '%27')) + '\')" href="' + link + '">';
10
+ el += '<a class="cover card-hover card-hover--spotlight card-hover--tilt" position="bottom" style="--cover-url:url(\'' + escape_html(bg.replace(/'/g, '%27')) + '\')" href="' + link + '">';
11
11
  el += '<img src="' + escape_html(bg) + '" alt=""/>';
12
12
  el += '<div class="cover-info" position="bottom" data-text-adaptive="split">';
13
13
  if (typeof label !== 'undefined' && label) {
@@ -17,7 +17,7 @@ function layoutDiv() {
17
17
  return el
18
18
  }
19
19
  el += `<div class="search-wrapper" id="search-wrapper">`
20
- el += `<form class="search-form">`
20
+ el += `<div class="search-form" role="search">`
21
21
  var filter = ''
22
22
  if (item.filter == 'auto') {
23
23
  if (page.wiki) {
@@ -31,20 +31,20 @@ function layoutDiv() {
31
31
  filter = item.filter
32
32
  }
33
33
  }
34
- el += `<a class="search-button" onclick="document.getElementById(&quot;search-input&quot;).focus();">`
34
+ if (filter.length > 0 && !filter.startsWith('/')) {
35
+ filter = '/' + filter
36
+ }
37
+ var placeholder = item.placeholder || (filter.length > 0 ? __('search.search_in', filter) : __('search.search'))
38
+ var escapedPlaceholder = escape_html(placeholder)
39
+ el += `<label class="search-button" for="search-input" aria-hidden="true">`
35
40
  el += icon('default:search', '', true)
36
- el += `</a>`
37
- el += `<input type="text" class="search-input" id="search-input"`
41
+ el += `</label>`
42
+ el += `<input type="search" class="search-input" id="search-input" aria-label="${escapedPlaceholder}"`
38
43
  if (filter.length > 0) {
39
- if (!filter.startsWith('/')) {
40
- filter = '/' + filter
41
- }
42
- el += ` data-filter="${filter}"`
43
- el += ` placeholder="${item.placeholder || __('search.search_in', filter)}">`
44
- } else {
45
- el += ` placeholder="${item.placeholder || __('search.search')}">`
44
+ el += ` data-filter="${escape_html(filter)}"`
46
45
  }
47
- el += `</form>`
46
+ el += ` placeholder="${escapedPlaceholder}">`
47
+ el += `</div>`
48
48
  el += `<div id="search-result"></div>`
49
49
  el += `<div class="search-no-result">${__('search.no_results')}</div>`
50
50
  el += `</div>`
@@ -0,0 +1,32 @@
1
+ <%
2
+ var spotlightColor = typeof conf.spotlight_color === 'string' && conf.spotlight_color.trim().length > 0
3
+ ? conf.spotlight_color.trim()
4
+ : 'rgba(255, 255, 255, 0.25)';
5
+ var maxTilt = Number(conf.max_tilt);
6
+ if (conf.max_tilt == null || conf.max_tilt === '' || !Number.isFinite(maxTilt)) {
7
+ maxTilt = 3;
8
+ }
9
+ maxTilt = Math.max(0, Math.min(8, maxTilt));
10
+ var cardHoverConfig = {
11
+ spotlightColor: spotlightColor,
12
+ maxTilt: maxTilt
13
+ };
14
+ var cardHoverConfigJson = JSON.stringify(cardHoverConfig).replace(/</g, '\\u003c');
15
+ %>
16
+ <script>
17
+ stellar.initPlugin(() => {
18
+ let disposed = false;
19
+ ctx.card_hover = <%- cardHoverConfigJson %>;
20
+ utils.js('/js/plugins/card-hover.js', { defer: true }).then(() => {
21
+ if (!disposed && stellar.cardHover && typeof stellar.cardHover.mountAll === 'function') {
22
+ stellar.cardHover.mountAll(document);
23
+ }
24
+ }).catch(() => {});
25
+ return () => {
26
+ disposed = true;
27
+ if (stellar.cardHover && typeof stellar.cardHover.destroy === 'function') {
28
+ stellar.cardHover.destroy();
29
+ }
30
+ };
31
+ }, 'card-hover');
32
+ </script>
@@ -29,6 +29,7 @@ function loadPlugins() {
29
29
 
30
30
  // search
31
31
  if (theme.search[theme.search.service]) {
32
+ el += `<script>window.addEventListener('DOMContentLoaded', function () { utils.js('/js/search/shortcut.js', { defer: true }); });</script>`;
32
33
  el += partial(`search/${theme.search.service}`, {conf: theme.search[theme.search.service]});
33
34
  }
34
35
 
package/layout/index.ejs CHANGED
@@ -85,9 +85,9 @@ function layout_post_card(layout, post, content) {
85
85
  if (hero) {
86
86
  // hero 封面卡片:背景图由 .post-card 自身背景承载(跟随 corner-shape 圆角裁剪),
87
87
  // 子图保留用于 hover 变暗/放大滤镜
88
- el += `<a class="post-card ${layout}" style="--cover-url:url('${escape_html(post.cover.replace(/'/g, '%27'))}')" href="${pretty_url(post.link || post.path)}">`
88
+ el += `<a class="post-card ${layout} card-hover card-hover--spotlight card-hover--tilt" style="--cover-url:url('${escape_html(post.cover.replace(/'/g, '%27'))}')" href="${pretty_url(post.link || post.path)}">`
89
89
  } else {
90
- el += `<a class="post-card ${layout}" href="${pretty_url(post.link || post.path)}">`
90
+ el += `<a class="post-card ${layout} card-hover card-hover--spotlight card-hover--tilt" href="${pretty_url(post.link || post.path)}">`
91
91
  }
92
92
  el += content;
93
93
  el += '</a>';
@@ -17,7 +17,7 @@ function layout_wiki_list(partial) {
17
17
  if (proj.cover) {
18
18
  coverStyle = ` style="--cover-url:url('${escape_html(proj.cover.replace(/'/g, '%27'))}')"`
19
19
  }
20
- el += `<a class="wiki-card${proj.cover ? '' : ' no-cover'}"${coverStyle} href="${pretty_url(proj.homepage?.path || '/')}">`
20
+ el += `<a class="wiki-card card-hover card-hover--spotlight card-hover--tilt${proj.cover ? '' : ' no-cover'}"${coverStyle} href="${pretty_url(proj.homepage?.path || '/')}">`
21
21
  el += partial(proj)
22
22
  el += `</a>`
23
23
  el += `</div>`
@@ -28,7 +28,7 @@ function layout_wiki_list(partial) {
28
28
  if (proj.cover) {
29
29
  coverStyle = ` style="--cover-url:url('${escape_html(proj.cover.replace(/'/g, '%27'))}')"`
30
30
  }
31
- el += `<a class="wiki-card${proj.cover ? '' : ' no-cover'}"${coverStyle} href="${pretty_url(proj.homepage?.path || '/')}">`
31
+ el += `<a class="wiki-card card-hover card-hover--spotlight card-hover--tilt${proj.cover ? '' : ' no-cover'}"${coverStyle} href="${pretty_url(proj.homepage?.path || '/')}">`
32
32
  el += partial(proj)
33
33
  el += `</a>`
34
34
  el += `</div>`
@@ -2,7 +2,7 @@
2
2
  <% for (const notebook of Object.values(theme.notebooks.tree)) { %>
3
3
  <div class="post-list wiki">
4
4
  <div class="post-card-wrap<%= scrollreveal(' ') %>">
5
- <a class="post-card wiki" href="<%= pretty_url(notebook.base_dir) %>">
5
+ <a class="post-card wiki card-hover card-hover--spotlight card-hover--tilt" href="<%= pretty_url(notebook.base_dir) %>">
6
6
  <%- include('_partial/main/notebook/notebook_card', { notebook: notebook }) %>
7
7
  </a>
8
8
  </div>
package/layout/notes.ejs CHANGED
@@ -3,7 +3,7 @@
3
3
  <% page.posts.each(post => { %>
4
4
  <div class="post-card-wrap<%= scrollreveal(' ') %>">
5
5
  <a
6
- class="post-card post"
6
+ class="post-card post card-hover card-hover--spotlight card-hover--tilt"
7
7
  href="<%= pretty_url(post.link || post.path) %><%= page.activeTag ? `?tag=${page.activeTag}` : '' %>"
8
8
  >
9
9
  <%- include('_partial/main/notebook/note_card', { note: post }) %>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hexo-theme-stellar",
3
- "version": "1.43.0",
3
+ "version": "1.44.0",
4
4
  "description": "Elegant and powerful theme for Hexo.",
5
5
  "main": "package.json",
6
6
  "scripts": {
@@ -48,7 +48,7 @@ module.exports = ctx => function(args, content = '') {
48
48
  el += '<div class="dropdown-menu ui-collection" data-ui-surface="glass" data-layout="list" data-variant="nav" data-density="compact">'
49
49
  for (const item of items) {
50
50
  const url = String(item.url)
51
- el += `<a class="dropdown-item ui-collection__item" href="${escapeHTML(toUrl(url))}`
51
+ el += `<a class="dropdown-item ui-collection__item card-hover card-hover--spotlight" href="${escapeHTML(toUrl(url))}`
52
52
  if (url.includes('://')) {
53
53
  el += '" target="_blank" rel="external nofollow noopener noreferrer">'
54
54
  } else {
@@ -18,6 +18,9 @@ module.exports = ctx => function(args, content) {
18
18
  args.w = '240px'
19
19
  }
20
20
  var el = ''
21
+ var cellClass = args.bg === 'card'
22
+ ? 'cell card-hover card-hover--spotlight card-hover--tilt'
23
+ : 'cell'
21
24
  el += '<div class="tag-plugin grid"'
22
25
  el += ' ' + ctx.args.joinTags(args, ['bg', 'columns']).join(' ')
23
26
  el += ' style="'
@@ -34,7 +37,7 @@ module.exports = ctx => function(args, content) {
34
37
  // 分组
35
38
  var cells = content.split(/<!--\s*cell(.*?)-->/g).filter(item => item.trim().length > 0)
36
39
  for (let cell of cells) {
37
- el += `<div class="cell" style="`
40
+ el += `<div class="${cellClass}" style="`
38
41
  if (args.br) {
39
42
  el += `border-radius:${args.br};`
40
43
  }
@@ -32,7 +32,7 @@ module.exports = ctx => function(args) {
32
32
  }
33
33
  var el = ''
34
34
  el += '<div class="tag-plugin link dis-select">'
35
- el += '<a class="link-card' + (args.desc ? ' rich' : ' plain') + '" title="' + safeTitle + '" href="' + args.url + '"'
35
+ el += '<a class="link-card' + (args.desc ? ' rich' : ' plain') + ' card-hover card-hover--spotlight card-hover--tilt" title="' + safeTitle + '" href="' + args.url + '"'
36
36
  if (args.url.includes('://')) {
37
37
  el += ' target="_blank" rel="external nofollow noopener noreferrer"'
38
38
  }
@@ -1,31 +1,3 @@
1
- .md-text .tag-list
2
- display: flex
3
- flex-wrap: wrap
4
- padding: 0
5
- margin-top: 2rem
6
- a.tag
7
- display: inline-flex
8
- align-items: center
9
- position: relative
10
- color: var(--text-p2)
11
- margin: 4px
12
- padding: .5em .75rem
13
- border-radius: 4px
14
- background: var(--block)
15
- font-size: $fs-13
16
- font-weight: 500
17
- &:before
18
- content: "#"
19
- margin-left: -2px
20
- margin-right: 2px
21
- opacity: .4
22
- &:hover
23
- &:before
24
- color var(--theme)
25
- opacity: 1
26
- color: var(--text)
27
- background: var(--block-border)
28
-
29
1
  .post-list .post-card .meta.cap .tag
30
2
  &:before
31
3
  content: "#"
@@ -97,14 +97,18 @@
97
97
  transform: scale(1.03)
98
98
  .wiki-cover-background-overlay
99
99
  position: absolute
100
- z-index: 2
100
+ z-index: 3
101
101
  inset: 0
102
102
  pointer-events: none
103
103
  background: linear-gradient(to bottom, transparent 0%, transparent 80%, var(--background) 100%)
104
104
  &.galaxy
105
- background: #000000
105
+ &:not(.image)
106
+ background: #000000
106
107
  canvas
107
108
  display: block
109
+ position: absolute
110
+ z-index: 2
111
+ inset: 0
108
112
  width: 100%
109
113
  height: 100%
110
114
  pointer-events: none
@@ -224,7 +228,7 @@
224
228
  border-color: var(--text-banner, var(--text))
225
229
  color: var(--text-banner, var(--text))
226
230
  background: var(--text-banner, var(--text))
227
- svg, span
231
+ > svg, > span:not(.card-hover__spotlight)
228
232
  filter: invert(1)
229
233
  &:hover
230
234
  color: var(--text-banner, var(--text))
@@ -22,8 +22,9 @@
22
22
  box-shadow: none
23
23
  img
24
24
  flex: initial
25
- width: auto
26
- height: auto
25
+ width: 24px
26
+ height: 24px
27
+ object-fit: contain
27
28
  border-radius: 2px
28
29
  svg
29
30
  flex: initial
@@ -53,12 +53,17 @@
53
53
  &:after
54
54
  opacity: 1
55
55
  .search-input
56
+ -webkit-appearance: none
57
+ appearance: none
56
58
  width: 100%
57
59
  box-sizing: border-box
58
60
  font-family: $ff-body
59
61
  font-size: $fs-14
60
62
  padding: 12px 0
61
63
  color: var(--text)
64
+ &::-webkit-search-cancel-button,
65
+ &::-webkit-search-decoration
66
+ -webkit-appearance: none
62
67
  &::placeholder
63
68
  color: var(--text-p3)
64
69
  :root[data-theme="dark"] &
@@ -74,6 +79,7 @@
74
79
  flex-shrink: 0
75
80
  width: 1.5rem
76
81
  height: 1.5rem
82
+ cursor: pointer
77
83
  svg
78
84
  height: 1rem
79
85
  width: 1rem
@@ -118,14 +124,11 @@
118
124
  list-style-type: none
119
125
  li a
120
126
  display: block
121
- background: var(--ui-summary-item-bg)
127
+ background: var(--ui-adapter-bg-hover)
128
+ box-shadow: var(--ui-adapter-shadow-hover)
122
129
  line-height: 1.2
123
130
  padding: 0.75rem 1rem
124
131
  border-radius: $border-card-s
125
- trans1 background
126
- &:hover
127
- background: var(--ui-adapter-bg-hover)
128
- box-shadow: var(--ui-adapter-shadow-hover)
129
132
  &:focus-visible
130
133
  outline: 2px solid var(--theme)
131
134
  outline-offset: 2px
@@ -0,0 +1,63 @@
1
+ // 组合类驱动的通用卡片 hover 能力。
2
+ // 仅在客户端完成挂载后生效,脚本失败时保留组件原有表现。
3
+ .card-hover.is-card-hover-ready
4
+ position: relative
5
+ --card-hover-mouse-x: 50%
6
+ --card-hover-mouse-y: 50%
7
+ --card-hover-rotate-x: 0deg
8
+ --card-hover-rotate-y: 0deg
9
+
10
+ .card-hover.is-card-hover-ready.card-hover--tilt
11
+ transform: perspective(900px) translate3d(0, 0, 0) rotateX(var(--card-hover-rotate-x)) rotateY(var(--card-hover-rotate-y))
12
+ transform-origin: center
13
+ transition: transform .25s ease-out, box-shadow .2s ease-out
14
+
15
+ .card-hover.is-card-hover-ready.card-hover--tilt.is-card-hover-active
16
+ transform: perspective(900px) translate3d(0, -2px, 0) rotateX(var(--card-hover-rotate-x)) rotateY(var(--card-hover-rotate-y))
17
+ transition-duration: .08s, .2s
18
+ will-change: transform
19
+
20
+ .card-hover__spotlight
21
+ position: absolute
22
+ inset: 0
23
+ z-index: 2
24
+ border-radius: inherit
25
+ corner-shape: inherit
26
+ background: radial-gradient(circle at var(--card-hover-mouse-x) var(--card-hover-mouse-y), var(--card-hover-spotlight-color, rgba(255, 255, 255, .25)), transparent 80%)
27
+ opacity: 0
28
+ pointer-events: none
29
+ transition: opacity .25s ease-out
30
+
31
+ .card-hover.is-card-hover-ready.card-hover--spotlight
32
+ &.is-card-hover-active, &:focus-within
33
+ > .card-hover__spotlight
34
+ opacity: .6
35
+
36
+ // ScrollReveal 使用外层 .post-card-wrap 的 transform;倾斜只作用于内层卡片。
37
+ // 启用插件时把同尺寸包装器的阴影移到卡片本体,使阴影随 3D transform 移动。
38
+ .post-list:not(.topic) .post-card-wrap
39
+ box-shadow: none
40
+
41
+ .post-list:not(.topic) .post-card.card-hover,
42
+ .post-list.wiki .wiki-card.card-hover
43
+ box-shadow: $boxshadow-card
44
+
45
+ .post-list.wiki .post-card-wrap
46
+ box-shadow: none
47
+
48
+ // link/grid 原本通过 hoverable-card() 写入 transform;显式组合类接管后统一合成倾斜与上浮。
49
+ .md-text .link-card.card-hover.card-hover--tilt.is-card-hover-ready,
50
+ .tag-plugin.grid[bg='card'] > .cell.card-hover.card-hover--tilt.is-card-hover-ready
51
+ transform: perspective(900px) translate3d(0, 0, 0) rotateX(var(--card-hover-rotate-x)) rotateY(var(--card-hover-rotate-y))
52
+ &.is-card-hover-active
53
+ transform: perspective(900px) translate3d(0, -2px, 0) rotateX(var(--card-hover-rotate-x)) rotateY(var(--card-hover-rotate-y))
54
+
55
+ @media (hover: none), (pointer: coarse), (prefers-reduced-motion: reduce)
56
+ .card-hover.is-card-hover-ready.card-hover--tilt,
57
+ .card-hover.is-card-hover-ready.card-hover--tilt.is-card-hover-active,
58
+ .md-text .link-card.card-hover.card-hover--tilt.is-card-hover-ready,
59
+ .tag-plugin.grid[bg='card'] > .cell.card-hover.card-hover--tilt.is-card-hover-ready
60
+ transform: none
61
+ transition: none
62
+ .card-hover.is-card-hover-ready.card-hover--spotlight > .card-hover__spotlight
63
+ opacity: 0
@@ -10,3 +10,6 @@ if hexo-config('plugins.tianli_gpt.enable')
10
10
  @import 'tianli_gpt'
11
11
  if hexo-config('plugins.katex.enable')
12
12
  @import 'katex'
13
+ if hexo-config('plugins.card_hover.enable')
14
+ // 最后加载,组合并覆盖 link/grid 既有 hover transform
15
+ @import 'card-hover'
@@ -0,0 +1,254 @@
1
+ /*
2
+ * Card hover spotlight adapted from React Bits Spotlight Card:
3
+ * https://github.com/DavidHDev/react-bits
4
+ */
5
+
6
+ (function () {
7
+ 'use strict';
8
+
9
+ window.stellar = window.stellar || {};
10
+
11
+ const DEFAULT_SPOTLIGHT_COLOR = 'rgba(255, 255, 255, 0.25)';
12
+ const DEFAULT_MAX_TILT = 3;
13
+ const MAX_TILT_LIMIT = 8;
14
+ const mounted = new Map();
15
+ const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
16
+ const finePointer = window.matchMedia('(hover: hover) and (pointer: fine)');
17
+ let lifecycleBound = false;
18
+
19
+ function normalizeConfig() {
20
+ const raw = typeof ctx !== 'undefined' && ctx.card_hover ? ctx.card_hover : {};
21
+ let maxTilt = Number(raw.maxTilt);
22
+ if (raw.maxTilt === null || raw.maxTilt === '' || !Number.isFinite(maxTilt)) {
23
+ maxTilt = DEFAULT_MAX_TILT;
24
+ }
25
+ maxTilt = Math.max(0, Math.min(MAX_TILT_LIMIT, maxTilt));
26
+
27
+ let spotlightColor = typeof raw.spotlightColor === 'string' && raw.spotlightColor.trim()
28
+ ? raw.spotlightColor.trim()
29
+ : DEFAULT_SPOTLIGHT_COLOR;
30
+ if (window.CSS && typeof window.CSS.supports === 'function' && !window.CSS.supports('color', spotlightColor)) {
31
+ spotlightColor = DEFAULT_SPOTLIGHT_COLOR;
32
+ }
33
+ return { maxTilt: maxTilt, spotlightColor: spotlightColor };
34
+ }
35
+
36
+ function canAnimate() {
37
+ return !reduceMotion.matches && finePointer.matches;
38
+ }
39
+
40
+ function requestFrame(state) {
41
+ if (state.frame !== null) return;
42
+ state.frame = window.requestAnimationFrame(function () {
43
+ state.frame = null;
44
+ if (!state.pointer || !document.documentElement.contains(state.element)) return;
45
+ const rect = state.element.getBoundingClientRect();
46
+ if (rect.width <= 0 || rect.height <= 0) return;
47
+
48
+ const x = Math.max(0, Math.min(rect.width, state.pointer.x - rect.left));
49
+ const y = Math.max(0, Math.min(rect.height, state.pointer.y - rect.top));
50
+ const normalizedX = x / rect.width * 2 - 1;
51
+ const normalizedY = y / rect.height * 2 - 1;
52
+
53
+ state.element.style.setProperty('--card-hover-mouse-x', x + 'px');
54
+ state.element.style.setProperty('--card-hover-mouse-y', y + 'px');
55
+ if (state.hasTilt) {
56
+ state.element.style.setProperty('--card-hover-rotate-x', (-normalizedY * state.maxTilt).toFixed(3) + 'deg');
57
+ state.element.style.setProperty('--card-hover-rotate-y', (normalizedX * state.maxTilt).toFixed(3) + 'deg');
58
+ }
59
+ });
60
+ }
61
+
62
+ function cancelFrame(state) {
63
+ if (state.frame !== null) {
64
+ window.cancelAnimationFrame(state.frame);
65
+ state.frame = null;
66
+ }
67
+ }
68
+
69
+ function resetSpotlightPosition(state) {
70
+ state.element.style.setProperty('--card-hover-mouse-x', '50%');
71
+ state.element.style.setProperty('--card-hover-mouse-y', '50%');
72
+ }
73
+
74
+ function deactivatePointer(state) {
75
+ cancelFrame(state);
76
+ state.pointer = null;
77
+ state.element.classList.remove('is-card-hover-active');
78
+ state.element.style.setProperty('--card-hover-rotate-x', '0deg');
79
+ state.element.style.setProperty('--card-hover-rotate-y', '0deg');
80
+ if (!state.spotlight || state.element.matches(':focus-within')) {
81
+ resetSpotlightPosition(state);
82
+ }
83
+ }
84
+
85
+ function resetState(state) {
86
+ deactivatePointer(state);
87
+ resetSpotlightPosition(state);
88
+ }
89
+
90
+ function unmount(state) {
91
+ resetState(state);
92
+ state.element.removeEventListener('pointerenter', state.onPointerEnter);
93
+ state.element.removeEventListener('pointermove', state.onPointerMove);
94
+ state.element.removeEventListener('pointerleave', state.onPointerLeave);
95
+ state.element.removeEventListener('focusin', state.onFocusIn);
96
+ if (state.spotlight) {
97
+ state.spotlight.removeEventListener('transitionend', state.onSpotlightTransitionEnd);
98
+ }
99
+ state.element.classList.remove('is-card-hover-ready');
100
+ if (state.spotlight && state.spotlight.parentNode === state.element) {
101
+ state.spotlight.remove();
102
+ }
103
+ mounted.delete(state.element);
104
+ }
105
+
106
+ function unmountAll(root) {
107
+ Array.from(mounted.values()).forEach(function (state) {
108
+ if (!root || state.element === root || (typeof root.contains === 'function' && root.contains(state.element))) {
109
+ unmount(state);
110
+ }
111
+ });
112
+ }
113
+
114
+ function mount(element) {
115
+ if (!element || mounted.has(element) || !canAnimate()) return;
116
+ const hasSpotlight = element.classList.contains('card-hover--spotlight');
117
+ const hasTilt = element.classList.contains('card-hover--tilt');
118
+ if (!hasSpotlight && !hasTilt) return;
119
+
120
+ const config = normalizeConfig();
121
+ document.documentElement.style.setProperty('--card-hover-spotlight-color', config.spotlightColor);
122
+ let spotlight = null;
123
+ if (hasSpotlight) {
124
+ spotlight = document.createElement('span');
125
+ spotlight.className = 'card-hover__spotlight';
126
+ spotlight.setAttribute('aria-hidden', 'true');
127
+ element.appendChild(spotlight);
128
+ }
129
+
130
+ const state = {
131
+ element: element,
132
+ spotlight: spotlight,
133
+ hasTilt: hasTilt,
134
+ maxTilt: config.maxTilt,
135
+ frame: null,
136
+ pointer: null,
137
+ onPointerEnter: null,
138
+ onPointerMove: null,
139
+ onPointerLeave: null,
140
+ onFocusIn: null,
141
+ onSpotlightTransitionEnd: null
142
+ };
143
+ state.onPointerEnter = function (event) {
144
+ state.pointer = { x: event.clientX, y: event.clientY };
145
+ element.classList.add('is-card-hover-active');
146
+ requestFrame(state);
147
+ };
148
+ state.onPointerMove = function (event) {
149
+ state.pointer = { x: event.clientX, y: event.clientY };
150
+ requestFrame(state);
151
+ };
152
+ state.onPointerLeave = function () {
153
+ deactivatePointer(state);
154
+ };
155
+ state.onFocusIn = function () {
156
+ if (!element.classList.contains('is-card-hover-active')) {
157
+ resetSpotlightPosition(state);
158
+ }
159
+ };
160
+ state.onSpotlightTransitionEnd = function (event) {
161
+ if (event.target !== spotlight || event.propertyName !== 'opacity') return;
162
+ if (element.classList.contains('is-card-hover-active') || element.matches(':focus-within')) return;
163
+ resetSpotlightPosition(state);
164
+ };
165
+
166
+ element.addEventListener('pointerenter', state.onPointerEnter, { passive: true });
167
+ element.addEventListener('pointermove', state.onPointerMove, { passive: true });
168
+ element.addEventListener('pointerleave', state.onPointerLeave, { passive: true });
169
+ element.addEventListener('focusin', state.onFocusIn);
170
+ if (spotlight) {
171
+ spotlight.addEventListener('transitionend', state.onSpotlightTransitionEnd);
172
+ }
173
+ element.classList.add('is-card-hover-ready');
174
+ mounted.set(element, state);
175
+ }
176
+
177
+ function mountAll(root) {
178
+ bindLifecycle();
179
+ if (!canAnimate()) {
180
+ unmountAll();
181
+ return;
182
+ }
183
+ const scope = root && typeof root.querySelectorAll === 'function' ? root : document;
184
+ if (scope.nodeType === 1 && scope.matches('.card-hover')) {
185
+ mount(scope);
186
+ }
187
+ scope.querySelectorAll('.card-hover').forEach(mount);
188
+ }
189
+
190
+ function resetAll() {
191
+ mounted.forEach(resetState);
192
+ }
193
+
194
+ function handleMotionPreference() {
195
+ if (canAnimate()) {
196
+ mountAll(document);
197
+ } else {
198
+ unmountAll();
199
+ }
200
+ }
201
+
202
+ function handleRenderedMarkdown(event) {
203
+ const target = event.detail && event.detail.target ? event.detail.target : document;
204
+ mountAll(target);
205
+ }
206
+
207
+ function handleVisibility() {
208
+ if (document.hidden) resetAll();
209
+ }
210
+
211
+ function addMediaListener(media, listener) {
212
+ if (typeof media.addEventListener === 'function') {
213
+ media.addEventListener('change', listener);
214
+ } else if (typeof media.addListener === 'function') {
215
+ media.addListener(listener);
216
+ }
217
+ }
218
+
219
+ function removeMediaListener(media, listener) {
220
+ if (typeof media.removeEventListener === 'function') {
221
+ media.removeEventListener('change', listener);
222
+ } else if (typeof media.removeListener === 'function') {
223
+ media.removeListener(listener);
224
+ }
225
+ }
226
+
227
+ function bindLifecycle() {
228
+ if (lifecycleBound) return;
229
+ lifecycleBound = true;
230
+ addMediaListener(reduceMotion, handleMotionPreference);
231
+ addMediaListener(finePointer, handleMotionPreference);
232
+ document.addEventListener('stellar:mdrender', handleRenderedMarkdown);
233
+ document.addEventListener('visibilitychange', handleVisibility);
234
+ window.addEventListener('pagehide', resetAll);
235
+ }
236
+
237
+ function destroy() {
238
+ unmountAll();
239
+ if (!lifecycleBound) return;
240
+ lifecycleBound = false;
241
+ removeMediaListener(reduceMotion, handleMotionPreference);
242
+ removeMediaListener(finePointer, handleMotionPreference);
243
+ document.removeEventListener('stellar:mdrender', handleRenderedMarkdown);
244
+ document.removeEventListener('visibilitychange', handleVisibility);
245
+ window.removeEventListener('pagehide', resetAll);
246
+ document.documentElement.style.removeProperty('--card-hover-spotlight-color');
247
+ }
248
+
249
+ stellar.cardHover = {
250
+ mountAll: mountAll,
251
+ unmountAll: unmountAll,
252
+ destroy: destroy
253
+ };
254
+ })();
@@ -168,7 +168,7 @@ void main() {
168
168
  }
169
169
  `;
170
170
 
171
- const params = {
171
+ const defaultParams = {
172
172
  focal: [0.5, 0.5],
173
173
  rotation: [1.0, 0.0],
174
174
  starSpeed: 2.0,
@@ -185,6 +185,69 @@ void main() {
185
185
  transparent: true
186
186
  };
187
187
 
188
+ function isFiniteNumber(value) {
189
+ return typeof value === 'number' && Number.isFinite(value);
190
+ }
191
+
192
+ function finiteOrDefault(value, fallback) {
193
+ return isFiniteNumber(value) ? value : fallback;
194
+ }
195
+
196
+ function nonNegativeOrDefault(value, fallback) {
197
+ return isFiniteNumber(value) && value >= 0 ? value : fallback;
198
+ }
199
+
200
+ function booleanOrDefault(value, fallback) {
201
+ return typeof value === 'boolean' ? value : fallback;
202
+ }
203
+
204
+ function pairOrDefault(value, fallback, clampToUnit) {
205
+ if (!Array.isArray(value) || value.length !== 2 || !isFiniteNumber(value[0]) || !isFiniteNumber(value[1])) {
206
+ return fallback.slice();
207
+ }
208
+ if (clampToUnit) {
209
+ return value.map(function (item) {
210
+ return Math.max(0, Math.min(1, item));
211
+ });
212
+ }
213
+ return value.slice();
214
+ }
215
+
216
+ function normalizeHue(value, fallback) {
217
+ if (!isFiniteNumber(value)) return fallback;
218
+ return ((value % 360) + 360) % 360;
219
+ }
220
+
221
+ function normalizeParams(value) {
222
+ const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
223
+ return {
224
+ focal: pairOrDefault(source.focal, defaultParams.focal, true),
225
+ rotation: pairOrDefault(source.rotation, defaultParams.rotation, false),
226
+ starSpeed: nonNegativeOrDefault(source.starSpeed, defaultParams.starSpeed),
227
+ density: nonNegativeOrDefault(source.density, defaultParams.density),
228
+ hueShift: normalizeHue(source.hueShift, defaultParams.hueShift),
229
+ speed: nonNegativeOrDefault(source.speed, defaultParams.speed),
230
+ glowIntensity: nonNegativeOrDefault(source.glowIntensity, defaultParams.glowIntensity),
231
+ saturation: nonNegativeOrDefault(source.saturation, defaultParams.saturation),
232
+ mouseRepulsion: booleanOrDefault(source.mouseRepulsion, defaultParams.mouseRepulsion),
233
+ twinkleIntensity: nonNegativeOrDefault(source.twinkleIntensity, defaultParams.twinkleIntensity),
234
+ rotationSpeed: finiteOrDefault(source.rotationSpeed, defaultParams.rotationSpeed),
235
+ repulsionStrength: nonNegativeOrDefault(source.repulsionStrength, defaultParams.repulsionStrength),
236
+ autoCenterRepulsion: nonNegativeOrDefault(source.autoCenterRepulsion, defaultParams.autoCenterRepulsion),
237
+ transparent: booleanOrDefault(source.transparent, defaultParams.transparent)
238
+ };
239
+ }
240
+
241
+ function readParams(canvas) {
242
+ const raw = canvas.getAttribute('data-galaxy-params');
243
+ if (!raw) return normalizeParams({});
244
+ try {
245
+ return normalizeParams(JSON.parse(raw));
246
+ } catch (e) {
247
+ return normalizeParams({});
248
+ }
249
+ }
250
+
188
251
  function createShader(gl, type, source) {
189
252
  const shader = gl.createShader(type);
190
253
  gl.shaderSource(shader, source);
@@ -235,6 +298,7 @@ void main() {
235
298
  const background = canvas.parentElement;
236
299
  const interactionTarget = canvas.closest('.wiki-hero') || background;
237
300
  if (!background || !interactionTarget) return;
301
+ const params = readParams(canvas);
238
302
 
239
303
  let gl;
240
304
  try {
@@ -9,6 +9,25 @@ utils.js(window.searchConfig.js).then(() => {
9
9
  var client = algoliasearch(window.searchConfig.appId, window.searchConfig.apiKey);
10
10
  var index = client.initIndex(window.searchConfig.indexName);
11
11
 
12
+ function getCardHoverApi() {
13
+ if (typeof stellar === 'undefined' || !stellar.cardHover) return null;
14
+ return stellar.cardHover;
15
+ }
16
+
17
+ function unmountResultCards(root) {
18
+ var cardHover = getCardHoverApi();
19
+ if (cardHover && typeof cardHover.unmountAll === 'function') {
20
+ cardHover.unmountAll(root);
21
+ }
22
+ }
23
+
24
+ function mountResultCards(root) {
25
+ var cardHover = getCardHoverApi();
26
+ if (cardHover && typeof cardHover.mountAll === 'function') {
27
+ cardHover.mountAll(root);
28
+ }
29
+ }
30
+
12
31
  function filterResults(hits, filterPath) {
13
32
  if (!filterPath || filterPath === '/') return hits;
14
33
  var regex = new RegExp(filterPath);
@@ -26,11 +45,27 @@ utils.js(window.searchConfig.js).then(() => {
26
45
  var contentSnippet = hit._snippetResult.content.value;
27
46
  var title = hit.hierarchy.lvl1 || 'Untitled';
28
47
  var item = document.createElement("li");
29
- item.innerHTML = `<a href="${hit.url}"><span class='search-result-title'>${title}</span><p class="search-result-content">${contentSnippet}</p></a>`;
48
+ var titleSpan = document.createElement("span");
49
+ titleSpan.className = "search-result-title";
50
+ titleSpan.textContent = title;
51
+
52
+ var link = document.createElement("a");
53
+ link.className = "card-hover card-hover--spotlight";
54
+ link.href = hit.url;
55
+
56
+ var content = document.createElement("p");
57
+ content.className = "search-result-content";
58
+ content.innerHTML = contentSnippet;
59
+
60
+ link.appendChild(content);
61
+ item.appendChild(titleSpan);
62
+ item.appendChild(link);
30
63
  resultList.appendChild(item);
31
64
  });
32
65
  }
66
+ unmountResultCards(resultArea);
33
67
  resultArea.replaceChildren(resultList);
68
+ mountResultCards(resultList);
34
69
  }
35
70
 
36
71
  inputArea.addEventListener("input", function() {
@@ -39,6 +74,7 @@ utils.js(window.searchConfig.js).then(() => {
39
74
 
40
75
  if (query.length <= 0) {
41
76
  searchWrapper.setAttribute('searching', 'false');
77
+ unmountResultCards(resultArea);
42
78
  resultArea.replaceChildren();
43
79
  return;
44
80
  }
@@ -129,6 +129,25 @@ var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
129
129
  });
130
130
  }
131
131
 
132
+ function getCardHoverApi() {
133
+ if (typeof stellar === 'undefined' || !stellar.cardHover) return null;
134
+ return stellar.cardHover;
135
+ }
136
+
137
+ function unmountResultCards(root) {
138
+ var cardHover = getCardHoverApi();
139
+ if (cardHover && typeof cardHover.unmountAll === 'function') {
140
+ cardHover.unmountAll(root);
141
+ }
142
+ }
143
+
144
+ function mountResultCards(root) {
145
+ var cardHover = getCardHoverApi();
146
+ if (cardHover && typeof cardHover.mountAll === 'function') {
147
+ cardHover.mountAll(root);
148
+ }
149
+ }
150
+
132
151
  // 构建单条搜索结果 DOM
133
152
  function buildResultElement(dataTitle, sectionName, secText, secFirst, pairs, href) {
134
153
  const li = document.createElement('li');
@@ -140,6 +159,7 @@ var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
140
159
  li.appendChild(titleSpan);
141
160
 
142
161
  const a = document.createElement('a');
162
+ a.className = 'card-hover card-hover--spotlight';
143
163
  a.href = href;
144
164
 
145
165
  if (sectionName) {
@@ -180,6 +200,7 @@ var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
180
200
 
181
201
  $input.addEventListener("input", function() {
182
202
  var rawValue = this.value.trim();
203
+ unmountResultCards($resultContent);
183
204
  $resultContent.innerHTML = "";
184
205
  if (rawValue.length <= 0) {
185
206
  $wrapper.setAttribute('searching', 'false');
@@ -283,6 +304,7 @@ var searchFunc = function(path, filter, wrapperId, searchId, contentId) {
283
304
 
284
305
  $resultContent.innerHTML = '';
285
306
  $resultContent.appendChild(ul);
307
+ mountResultCards(ul);
286
308
  }
287
309
  });
288
310
 
@@ -0,0 +1,39 @@
1
+ (function() {
2
+ function isSearchShortcut(event) {
3
+ if (event.defaultPrevented || event.isComposing) return false;
4
+ if (event.altKey || event.shiftKey) return false;
5
+ if (!event.metaKey && !event.ctrlKey) return false;
6
+ return typeof event.key === 'string' && event.key.toLowerCase() === 'k';
7
+ }
8
+
9
+ function isEditableTarget(target) {
10
+ if (!target || typeof target.closest !== 'function') return false;
11
+ return target.closest('input, textarea, select, [contenteditable]:not([contenteditable="false"])') !== null;
12
+ }
13
+
14
+ function isNarrowLayout() {
15
+ var leftbarToggle = document.querySelector('.mobile-only.leftbar-toggle');
16
+ return !!(leftbarToggle && window.getComputedStyle(leftbarToggle).display !== 'none');
17
+ }
18
+
19
+ function handleShortcut(event) {
20
+ if (!isSearchShortcut(event)) return;
21
+
22
+ var input = document.getElementById('search-input');
23
+ if (!input || isNarrowLayout()) return;
24
+ if (event.target !== input && isEditableTarget(event.target)) return;
25
+
26
+ event.preventDefault();
27
+ input.focus();
28
+ }
29
+
30
+ function init() {
31
+ document.addEventListener('keydown', handleShortcut);
32
+ }
33
+
34
+ if (document.readyState === 'loading') {
35
+ document.addEventListener('DOMContentLoaded', init, { once: true });
36
+ } else {
37
+ init();
38
+ }
39
+ })();