@abreen/tada 1.5.8 → 1.6.2

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/build/watch.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  getPublicDir,
13
13
  getDistDir,
14
14
  getPackageDir,
15
+ toPosix,
15
16
  } from './utils/paths';
16
17
  import { isFeatureEnabled } from './features';
17
18
  import { bundle } from './bundle';
@@ -55,10 +56,7 @@ export async function runWatch(options: {
55
56
  define: { __WEBSOCKET_PORT__: String(WEBSOCKET_PORT) },
56
57
  });
57
58
  return result.outputs.map(output =>
58
- path
59
- .relative(getDistDir(), output.path)
60
- .split(path.sep)
61
- .join(path.posix.sep),
59
+ toPosix(path.relative(getDistDir(), output.path)),
62
60
  );
63
61
  }
64
62
 
@@ -154,10 +152,7 @@ export async function runWatch(options: {
154
152
  return null;
155
153
  }
156
154
 
157
- return path
158
- .relative(contentDir, normalizedFilePath)
159
- .split(path.sep)
160
- .join(path.posix.sep);
155
+ return toPosix(path.relative(contentDir, normalizedFilePath));
161
156
  }
162
157
 
163
158
  function toPublicRelativePath(filePath: string): string | null {
@@ -169,10 +164,7 @@ export async function runWatch(options: {
169
164
  if (!normalizedFilePath.startsWith(normalizedPublicDir)) {
170
165
  return null;
171
166
  }
172
- return path
173
- .relative(publicDir, normalizedFilePath)
174
- .split(path.sep)
175
- .join(path.posix.sep);
167
+ return toPosix(path.relative(publicDir, normalizedFilePath));
176
168
  }
177
169
 
178
170
  // Watch mode
@@ -354,10 +346,7 @@ export async function runWatch(options: {
354
346
  const absPath = path.resolve(filePath);
355
347
  if (fs.existsSync(absPath)) {
356
348
  copyPublicFile(publicDir, distDir, absPath, contentAssetRelPaths);
357
- const rel = path
358
- .relative(publicDir, absPath)
359
- .split(path.sep)
360
- .join(path.posix.sep);
349
+ const rel = toPosix(path.relative(publicDir, absPath));
361
350
  publicRelPaths.add(rel);
362
351
  }
363
352
  }
@@ -397,10 +386,7 @@ export async function runWatch(options: {
397
386
  const absPath = path.resolve(filePath);
398
387
  if (fs.existsSync(absPath)) {
399
388
  copyPublicFile(publicDir, distDir, absPath, contentAssetRelPaths);
400
- const rel = path
401
- .relative(publicDir, absPath)
402
- .split(path.sep)
403
- .join(path.posix.sep);
389
+ const rel = toPosix(path.relative(publicDir, absPath));
404
390
  publicRelPaths.add(rel);
405
391
  }
406
392
  }
@@ -417,10 +403,7 @@ export async function runWatch(options: {
417
403
  const ext = path.extname(absPath).slice(1).toLowerCase();
418
404
  if (!processedExtSet.has(ext) && fs.existsSync(absPath)) {
419
405
  copyContentFile(contentDir, distDir, absPath, publicRelPaths);
420
- const rel = path
421
- .relative(contentDir, absPath)
422
- .split(path.sep)
423
- .join(path.posix.sep);
406
+ const rel = toPosix(path.relative(contentDir, absPath));
424
407
  contentAssetRelPaths.add(rel);
425
408
  }
426
409
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.5.8",
3
+ "version": "1.6.2",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
@@ -29,10 +29,10 @@
29
29
  "prepack": "bun run scripts/prepack.ts",
30
30
  "lint": "eslint src/ build/ bin/",
31
31
  "typecheck": "tsc --noEmit",
32
- "test:functional": "python3 -m pip install -q -r functional_tests/requirements.txt && python3 -m pytest functional_tests/ -v -n auto --durations=10",
33
- "test:all": "bun test && bun run test:playwright && bun run test:functional",
34
- "test:coverage": "rm -rf coverage/unit coverage/functional && bun test --coverage && TADA_COVERAGE=1 python3 -m pip install -q -r functional_tests/requirements.txt && TADA_COVERAGE=1 python3 -m pytest functional_tests/ -v -n auto --durations=10 && bun run scripts/coverage-report.ts",
35
- "test:playwright": "bunx playwright test",
32
+ "test:functional": "bun run scripts/test.ts functional",
33
+ "test:all": "bun run scripts/test.ts all",
34
+ "test:coverage": "bun run scripts/test.ts coverage",
35
+ "test:playwright": "bun run scripts/test.ts playwright",
36
36
  "init-example": "bun run bin/tada.ts init example --no-interactive",
37
37
  "iew": "if [ -d example ]; then cd example; else bun run init-example && cd example; fi && ../bin/tada.ts watch"
38
38
  },
@@ -41,7 +41,8 @@
41
41
  },
42
42
  "os": [
43
43
  "darwin",
44
- "linux"
44
+ "linux",
45
+ "win32"
45
46
  ],
46
47
  "dependencies": {
47
48
  "@vscode/markdown-it-katex": "^1.1.2",
@@ -77,7 +78,7 @@
77
78
  },
78
79
  "devDependencies": {
79
80
  "@eslint/js": "^10.0.1",
80
- "@playwright/test": "^1.59.1",
81
+ "@playwright/test": "1.56.1",
81
82
  "@saintedlama/lcov-parse": "^1.0.1",
82
83
  "@types/bun": "^1.3.10",
83
84
  "@types/culori": "^4.0.1",
package/src/_alerts.scss CHANGED
@@ -39,10 +39,15 @@
39
39
  }
40
40
 
41
41
  a {
42
- text-decoration-color: var(--fg-color);
42
+ @include link-underline;
43
+ color: var(--fg-color);
43
44
 
44
45
  &:hover {
45
- text-decoration-color: inherit;
46
+ color: var(--fg2-color);
47
+ }
48
+
49
+ &:active {
50
+ color: var(--link-active-color);
46
51
  }
47
52
  }
48
53
 
@@ -60,6 +65,7 @@
60
65
  }
61
66
 
62
67
  .alert .title::before {
68
+ content: '';
63
69
  left: 8px;
64
70
  top: 8px;
65
71
  position: absolute;
@@ -68,12 +74,11 @@
68
74
  }
69
75
 
70
76
  .alert.warning .title::before {
71
- content: var(--icon-warning);
77
+ @include contained-mask(var(--icon-warning));
72
78
  }
73
79
 
74
80
  i.warning {
75
- background: var(--icon-warning);
76
- background-size: contain;
81
+ @include contained-mask(var(--icon-warning));
77
82
  display: inline-block;
78
83
  height: 1em;
79
84
  width: 1em;
@@ -81,7 +86,7 @@ i.warning {
81
86
  }
82
87
 
83
88
  .alert.note .title::before {
84
- content: var(--icon-note);
89
+ @include contained-mask(var(--icon-note));
85
90
  }
86
91
 
87
92
  .alert .title {
package/src/_base.scss CHANGED
@@ -66,22 +66,18 @@ html {
66
66
  }
67
67
 
68
68
  a {
69
- color: var(--fg-color);
70
- text-decoration-color: var(--fg2-color);
71
- text-decoration-thickness: 1.5px;
69
+ color: var(--link-color);
70
+ text-decoration-line: none;
72
71
 
73
72
  @media (hover: hover) {
74
73
  &:hover {
75
- color: var(--fg2-color);
74
+ color: var(--link-color-hover);
75
+ @include link-underline;
76
76
  }
77
77
  }
78
- }
79
78
 
80
- a:active {
81
- color: var(--link-active-color);
82
-
83
- @media (hover: hover) {
84
- text-decoration-color: var(--link-active-color);
79
+ &:active {
80
+ color: var(--link-active-color);
85
81
  }
86
82
  }
87
83
 
@@ -146,6 +142,7 @@ details .content {
146
142
  details summary {
147
143
  &:hover {
148
144
  color: var(--fg2-color);
145
+ @include link-underline;
149
146
  }
150
147
 
151
148
  &:active {
package/src/_layout.scss CHANGED
@@ -156,10 +156,31 @@ body.literate main table {
156
156
  .text {
157
157
  color: var(--fg2-color);
158
158
  text-decoration: underline;
159
- text-decoration-color: var(--fg2-color);
160
159
  text-decoration-thickness: 1px;
161
160
  text-underline-offset: 3px;
162
161
  }
162
+
163
+ &:hover .text {
164
+ color: var(--link-color-hover);
165
+ }
166
+
167
+ &:active .text {
168
+ color: var(--link-active-color);
169
+ }
170
+
171
+ &:active .avatar {
172
+ border-color: var(--link-active-color);
173
+ }
174
+
175
+ &:focus-visible {
176
+ outline: none;
177
+ }
178
+
179
+ &:focus-visible .text,
180
+ &:focus-visible .avatar {
181
+ outline: 2px solid var(--theme-color);
182
+ outline-offset: 2px;
183
+ }
163
184
  }
164
185
 
165
186
  .avatar {
@@ -167,6 +188,7 @@ body.literate main table {
167
188
  height: 24px;
168
189
  vertical-align: middle;
169
190
  margin-left: 4px;
191
+ border: 1px solid transparent;
170
192
  }
171
193
  }
172
194
  }
package/src/_mixins.scss CHANGED
@@ -2,3 +2,44 @@
2
2
  -webkit-user-select: $val;
3
3
  user-select: $val;
4
4
  }
5
+
6
+ @mixin link-underline {
7
+ text-decoration-line: underline;
8
+ text-decoration-color: currentColor;
9
+ text-decoration-thickness: 1px;
10
+ text-underline-offset: 0.11rem;
11
+ }
12
+
13
+ @mixin subdued-link {
14
+ color: var(--fg-color);
15
+ text-decoration-line: none;
16
+
17
+ @media (hover: hover) {
18
+ &:hover {
19
+ color: var(--fg2-color);
20
+ @include link-underline;
21
+ }
22
+ }
23
+
24
+ &:active {
25
+ color: var(--link-active-color);
26
+ }
27
+ }
28
+
29
+ @mixin contained-icon {
30
+ background-size: contain;
31
+ background-repeat: no-repeat;
32
+ background-position: center center;
33
+ }
34
+
35
+ @mixin contained-mask($url) {
36
+ -webkit-mask-image: $url;
37
+ mask-image: $url;
38
+ -webkit-mask-size: contain;
39
+ mask-size: contain;
40
+ -webkit-mask-repeat: no-repeat;
41
+ mask-repeat: no-repeat;
42
+ -webkit-mask-position: center center;
43
+ mask-position: center center;
44
+ background-color: currentColor;
45
+ }
@@ -1,40 +1,33 @@
1
- .heading-anchor {
1
+ @use '../mixins' as *;
2
+
3
+ a.heading-anchor {
2
4
  position: relative;
3
5
  display: inline-block;
4
- color: var(--fg-color);
5
- text-decoration: none;
6
+ @include subdued-link;
6
7
  }
7
8
 
8
- .heading-anchor::after {
9
- content: var(--icon-anchor);
9
+ a.heading-anchor::after {
10
+ content: '';
10
11
  display: inline-block;
11
- height: 0.75em;
12
- width: 0.75em;
12
+ height: 1em;
13
+ width: 1em;
13
14
  margin-left: 4px;
15
+ vertical-align: middle;
14
16
  opacity: 0;
17
+ @include contained-mask(var(--icon-anchor));
15
18
  }
16
19
 
17
- .heading-anchor:focus::after,
18
- .heading-anchor:focus-visible::after {
20
+ a.heading-anchor:focus-visible::after {
19
21
  opacity: 1;
20
22
  }
21
23
 
22
24
  @media (hover: hover) {
23
- h1:hover .heading-anchor::after,
24
- h2:hover .heading-anchor::after,
25
- h3:hover .heading-anchor::after,
26
- h4:hover .heading-anchor::after,
27
- h5:hover .heading-anchor::after,
28
- h6:hover .heading-anchor::after {
25
+ h1:hover a.heading-anchor::after,
26
+ h2:hover a.heading-anchor::after,
27
+ h3:hover a.heading-anchor::after,
28
+ h4:hover a.heading-anchor::after,
29
+ h5:hover a.heading-anchor::after,
30
+ h6:hover a.heading-anchor::after {
29
31
  opacity: 1;
30
32
  }
31
-
32
- h1:hover .heading-anchor,
33
- h2:hover .heading-anchor,
34
- h3:hover .heading-anchor,
35
- h4:hover .heading-anchor,
36
- h5:hover .heading-anchor,
37
- h6:hover .heading-anchor {
38
- color: var(--fg-color);
39
- }
40
33
  }
@@ -1,3 +1,5 @@
1
+ @use '../mixins' as *;
2
+
1
3
  :root {
2
4
  --header-collapsed-height: 45px;
3
5
  }
@@ -31,6 +33,12 @@ header details summary {
31
33
  @media (hover: hover) {
32
34
  &:hover {
33
35
  color: inherit;
36
+ text-decoration-line: none;
37
+ background: var(--bg2-color);
38
+ }
39
+
40
+ &:hover .site-title {
41
+ @include link-underline;
34
42
  }
35
43
  }
36
44
 
@@ -26,20 +26,7 @@ header nav ul li {
26
26
  header nav a {
27
27
  border: none;
28
28
  display: inline;
29
- color: var(--fg-color);
30
- text-decoration: none;
31
- }
32
-
33
- @media (hover: hover) {
34
- header nav a {
35
- &:hover {
36
- color: var(--fg2-color);
37
- }
38
-
39
- &:active {
40
- color: var(--link-active-color);
41
- }
42
- }
29
+ @include subdued-link;
43
30
  }
44
31
 
45
32
  header nav a.external {
@@ -60,10 +47,6 @@ header nav div > p {
60
47
  }
61
48
 
62
49
  @media (hover: hover) {
63
- header details summary:hover {
64
- background: var(--bg2-color);
65
- }
66
-
67
50
  header:has(details[open]):hover {
68
51
  background: var(--bg-color-translucent);
69
52
  }
package/src/index.ts CHANGED
@@ -59,6 +59,18 @@ document.addEventListener('DOMContentLoaded', async () => {
59
59
  // Mount per-page components
60
60
  await mountPerPageComponents(window);
61
61
 
62
+ // On reload with manual scrollRestoration, browsers skip the initial
63
+ // fragment scroll. Do it ourselves once per-page components have
64
+ // finished mutating the DOM. On a fresh cold load the browser already
65
+ // scrolled, so this is a harmless re-align.
66
+ if (window.location.hash) {
67
+ const id = window.location.hash.slice(1);
68
+ const el = window.document.getElementById(id);
69
+ if (el) {
70
+ el.scrollIntoView();
71
+ }
72
+ }
73
+
62
74
  for (const [name, reason] of Object.entries(failed)) {
63
75
  console.error(`Failed to mount ${name} component:`, reason);
64
76
  }
@@ -5,6 +5,11 @@ let currentAbortController: AbortController | null = null;
5
5
  let historyIndex = 0;
6
6
  let currentPath = '';
7
7
 
8
+ // In-memory scroll positions keyed by navIndex. Populated on scroll and
9
+ // consulted by handlePopState so returning to a previous entry via
10
+ // back/forward restores the user's scroll, not just scroll 0.
11
+ const scrollByIndex = new Map<number, number>();
12
+
8
13
  type Direction = 'forward' | 'back';
9
14
 
10
15
  function findAnchor(event: MouseEvent): HTMLAnchorElement | null {
@@ -145,27 +150,35 @@ async function performNavigation(
145
150
  teardownPerPageComponents();
146
151
  swapContent(newDoc);
147
152
  updateHead(newDoc);
153
+ const parsed = new URL(url);
154
+ currentPath = parsed.pathname + parsed.search;
155
+
148
156
  if (isPush) {
149
157
  historyIndex++;
150
- window.history.pushState({ navIndex: historyIndex }, '', url);
158
+ // Push without the hash; we add it back below via location.replace
159
+ // so the browser performs a real fragment navigation that updates
160
+ // :target and fires hashchange. pushState alone would not do
161
+ // either of those things.
162
+ const urlWithoutHash = parsed.origin + parsed.pathname + parsed.search;
163
+ window.history.pushState({ navIndex: historyIndex }, '', urlWithoutHash);
151
164
  }
152
- const parsed = new URL(url);
153
- currentPath = parsed.pathname + parsed.search;
154
165
 
155
166
  if (typeof scrollTarget === 'string') {
156
- // Hash target
157
- const el = document.getElementById(scrollTarget);
158
- if (el) {
159
- el.scrollIntoView();
160
- } else {
161
- window.scrollTo({ top: 0 });
162
- }
167
+ // Trigger a real fragment navigation. location.replace overwrites
168
+ // the current entry's URL in place and runs fragment navigation
169
+ // (updating :target, firing hashchange, scrolling to the element).
170
+ // It wipes the entry's state object, so reapply it via
171
+ // replaceState.
172
+ const urlWithHash = parsed.pathname + parsed.search + '#' + scrollTarget;
173
+ window.location.replace(urlWithHash);
174
+ window.history.replaceState({ navIndex: historyIndex }, '', urlWithHash);
163
175
  } else if (typeof scrollTarget === 'number') {
164
176
  // Restore scroll position (popstate)
165
177
  window.scrollTo({ top: scrollTarget });
166
178
  } else {
167
179
  window.scrollTo({ top: 0 });
168
180
  }
181
+ scrollByIndex.set(historyIndex, window.scrollY);
169
182
  };
170
183
 
171
184
  const doc = document as Document & {
@@ -261,6 +274,16 @@ async function performNavigation(
261
274
  export default function mountNavigate(window: Window): () => void {
262
275
  window.history.scrollRestoration = 'manual';
263
276
  currentPath = window.location.pathname + window.location.search;
277
+ scrollByIndex.set(historyIndex, window.scrollY);
278
+
279
+ // Track scroll position on every scroll event. We keep the latest
280
+ // scrollY per navIndex so that back/forward navigation can restore
281
+ // where the user was, not just scroll 0. Map writes are cheap; no
282
+ // need to debounce.
283
+ const saveScroll = () => {
284
+ scrollByIndex.set(historyIndex, window.scrollY);
285
+ };
286
+ window.addEventListener('scroll', saveScroll, { passive: true });
264
287
 
265
288
  function handleClick(event: MouseEvent) {
266
289
  const anchor = findAnchor(event);
@@ -278,7 +301,8 @@ export default function mountNavigate(window: Window): () => void {
278
301
  return;
279
302
  }
280
303
 
281
- // Same-page hash links: smooth scroll instead of letting the browser jump
304
+ // Same-page hash links: handle locally so we can clear search and
305
+ // close the header without a full SPA fetch.
282
306
  const url = new URL(anchor.href);
283
307
  if (
284
308
  url.pathname === window.location.pathname &&
@@ -293,11 +317,11 @@ export default function mountNavigate(window: Window): () => void {
293
317
  details.open = false;
294
318
  }
295
319
  if (url.hash) {
296
- window.history.pushState(null, '', url.hash);
297
- const el = window.document.getElementById(url.hash.slice(1));
298
- if (el) {
299
- el.scrollIntoView({ behavior: 'smooth' });
300
- }
320
+ // location.hash assignment performs a real fragment navigation:
321
+ // updates :target, fires hashchange (which the TOC listens for),
322
+ // creates a new history entry, and scrolls to the element.
323
+ // pushState would do none of those things.
324
+ window.location.hash = url.hash.slice(1);
301
325
  }
302
326
  return;
303
327
  }
@@ -314,15 +338,8 @@ export default function mountNavigate(window: Window): () => void {
314
338
  details.open = false;
315
339
  }
316
340
 
317
- // Save current scroll position and history index
318
- window.history.replaceState(
319
- {
320
- ...window.history.state,
321
- scrollY: window.scrollY,
322
- navIndex: historyIndex,
323
- },
324
- '',
325
- );
341
+ // Save current scroll position for the entry we're leaving
342
+ scrollByIndex.set(historyIndex, window.scrollY);
326
343
 
327
344
  const hash = url.hash ? url.hash.slice(1) : null;
328
345
 
@@ -337,23 +354,23 @@ export default function mountNavigate(window: Window): () => void {
337
354
  if (hash) {
338
355
  const el = window.document.getElementById(hash);
339
356
  if (el) {
340
- el.scrollIntoView({ behavior: 'smooth' });
357
+ el.scrollIntoView();
341
358
  }
342
359
  } else {
343
- window.scrollTo({ top: 0, behavior: 'smooth' });
360
+ window.scrollTo({ top: 0 });
344
361
  }
345
362
  return;
346
363
  }
347
364
  currentPath = newPath;
348
365
 
349
- const scrollY = event.state?.scrollY ?? null;
350
366
  const targetIndex = event.state?.navIndex ?? 0;
351
367
  const direction: Direction =
352
368
  targetIndex >= historyIndex ? 'forward' : 'back';
353
369
  historyIndex = targetIndex;
370
+ const savedY = scrollByIndex.get(targetIndex);
354
371
  performNavigation(
355
372
  window.location.href,
356
- typeof scrollY === 'number' ? scrollY : null,
373
+ typeof savedY === 'number' ? savedY : null,
357
374
  direction,
358
375
  false,
359
376
  );
@@ -367,5 +384,6 @@ export default function mountNavigate(window: Window): () => void {
367
384
  return () => {
368
385
  window.document.removeEventListener('click', handleClick, true);
369
386
  window.removeEventListener('popstate', handlePopState);
387
+ window.removeEventListener('scroll', saveScroll);
370
388
  };
371
389
  }
@@ -65,7 +65,7 @@
65
65
  width: 100%;
66
66
  }
67
67
 
68
- .heading-anchor::after {
68
+ a.heading-anchor::after {
69
69
  display: none;
70
70
  }
71
71
 
package/src/style.scss CHANGED
@@ -55,42 +55,23 @@ a.disabled {
55
55
  @media (hover: hover) {
56
56
  &:hover {
57
57
  color: var(--fg-color-translucent);
58
+ text-decoration-line: none;
58
59
  }
59
60
  }
60
61
  }
61
62
 
62
- a.external::after {
63
- content: '';
64
- background: var(--icon-external-link);
65
- background-size: contain;
66
- background-repeat: no-repeat;
67
- background-position: center center;
68
- margin-left: 4px;
69
- padding-right: 18px;
70
- }
71
-
72
- @media (hover: hover) {
73
- a.external:hover::after {
74
- background: var(--icon-external-link-hover);
75
- background-size: contain;
76
- background-repeat: no-repeat;
77
- background-position: center center;
78
- }
79
- }
80
-
81
- a.external:active::after {
82
- background: var(--icon-external-link-active) !important;
83
- background-size: contain !important;
84
- background-repeat: no-repeat !important;
85
- background-position: center center !important;
63
+ .external-link-tail {
64
+ white-space: nowrap;
86
65
  }
87
66
 
88
- a.external.disabled::after,
89
- a.external.disabled:active::after {
90
- background: var(--icon-external-translucent) !important;
91
- background-size: contain !important;
92
- background-repeat: no-repeat !important;
93
- background-position: center center !important;
67
+ .external-link-tail::after,
68
+ a.external:not(:has(.external-link-tail))::after {
69
+ content: '';
70
+ display: inline-block;
71
+ width: 1em;
72
+ height: 1em;
73
+ vertical-align: middle;
74
+ @include contained-mask(var(--icon-external-link));
94
75
  }
95
76
 
96
77
  .footnotes {
@@ -379,21 +360,6 @@ dl {
379
360
  dt {
380
361
  font-weight: 600;
381
362
  color: var(--theme-color-text);
382
-
383
- a {
384
- color: var(--theme-color-text);
385
- text-decoration-color: var(--theme-color-text);
386
-
387
- @media (hover: hover) {
388
- &:hover {
389
- text-decoration-color: var(--fg2-color);
390
- }
391
- }
392
-
393
- &:active {
394
- text-decoration-color: var(--link-active-color);
395
- }
396
- }
397
363
  }
398
364
 
399
365
  dd > *:first-child {
@@ -407,12 +373,17 @@ dfn {
407
373
  font-style: italic;
408
374
  }
409
375
 
410
- .breadcrumb {
376
+ a.breadcrumb {
411
377
  @include user-select(none);
378
+ @include subdued-link;
412
379
 
413
380
  &::after {
414
- content: var(--icon-breadcrumb);
381
+ content: '';
382
+ display: inline-block;
383
+ width: 1em;
384
+ height: 1em;
415
385
  vertical-align: middle;
386
+ @include contained-mask(var(--icon-breadcrumb));
416
387
  }
417
388
  }
418
389