@abreen/tada 1.16.8 → 1.16.10

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.
@@ -68,16 +68,6 @@ export const MATERIAL_SYMBOLS = Object.freeze({
68
68
  symbol: 'smart_display',
69
69
  opticalSize: 24,
70
70
  }),
71
- headerMenu: Object.freeze({
72
- cssVariable: '--icon-header-menu',
73
- symbol: 'menu',
74
- opticalSize: 24,
75
- }),
76
- headerClose: Object.freeze({
77
- cssVariable: '--icon-header-close',
78
- symbol: 'close',
79
- opticalSize: 24,
80
- }),
81
71
  } as const);
82
72
 
83
73
  type MaterialSymbol = (typeof MATERIAL_SYMBOLS)[keyof typeof MATERIAL_SYMBOLS];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abreen/tada",
3
- "version": "1.16.8",
3
+ "version": "1.16.10",
4
4
  "type": "module",
5
5
  "description": "A static site generator",
6
6
  "license": "MIT",
@@ -4,6 +4,31 @@ export type ContrastPreference = 'standard' | 'high';
4
4
  const FONT_STORAGE_KEY = 'fontPreference';
5
5
  const CONTRAST_STORAGE_KEY = 'contrastPreference';
6
6
 
7
+ interface FontPreferenceRequest {
8
+ generation: number;
9
+ preference: FontPreference;
10
+ promise: Promise<void>;
11
+ }
12
+
13
+ interface FontPreferenceLoader {
14
+ supported: boolean;
15
+ pending: FontPreferenceRequest | null;
16
+ failedPreference: FontPreference | null;
17
+ request(preference: FontPreference): FontPreferenceRequest | null;
18
+ isCurrent(request: FontPreferenceRequest): boolean;
19
+ complete(request: FontPreferenceRequest): void;
20
+ fail(request: FontPreferenceRequest): void;
21
+ cancel(): void;
22
+ }
23
+
24
+ function getFontPreferenceLoader(
25
+ window: Window,
26
+ ): FontPreferenceLoader | undefined {
27
+ return (
28
+ window as Window & { __tadaFontPreferenceLoader?: FontPreferenceLoader }
29
+ ).__tadaFontPreferenceLoader;
30
+ }
31
+
7
32
  function getStorage(window: Window): Storage | null {
8
33
  try {
9
34
  return window.localStorage;
@@ -91,6 +116,12 @@ function getDefaultFontPreference(document: Document): FontPreference {
91
116
  : 'sans';
92
117
  }
93
118
 
119
+ function getAppliedFontPreference(document: Document): FontPreference {
120
+ return document.documentElement.dataset.fontPreference === 'serif'
121
+ ? 'serif'
122
+ : 'sans';
123
+ }
124
+
94
125
  function getDefaultContrastPreference(window: Window): ContrastPreference {
95
126
  const configured =
96
127
  window.document.documentElement.dataset.defaultContrastPreference === 'high'
@@ -138,21 +169,19 @@ export function applyContrastPreference(
138
169
  }
139
170
  }
140
171
 
141
- function syncControls(
172
+ function syncSwitch(
142
173
  document: Document,
143
174
  selector: string,
144
- preference: string,
175
+ checked: boolean,
176
+ title: string,
145
177
  ): void {
146
- document.querySelectorAll<HTMLButtonElement>(selector).forEach(button => {
147
- button.disabled = false;
148
- button.setAttribute(
149
- 'aria-pressed',
150
- String(
151
- button.dataset.fontPreferenceValue === preference ||
152
- button.dataset.contrastPreferenceValue === preference,
153
- ),
154
- );
155
- });
178
+ const control = document.querySelector<HTMLButtonElement>(selector);
179
+ if (!control) {
180
+ return;
181
+ }
182
+ control.disabled = false;
183
+ control.setAttribute('aria-checked', String(checked));
184
+ control.title = title;
156
185
  }
157
186
 
158
187
  function syncAppearance(
@@ -160,11 +189,19 @@ function syncAppearance(
160
189
  fontPreference: FontPreference,
161
190
  contrastPreference: ContrastPreference,
162
191
  ): void {
163
- syncControls(document, '[data-font-preference-value]', fontPreference);
164
- syncControls(
192
+ syncSwitch(
165
193
  document,
166
- '[data-contrast-preference-value]',
167
- contrastPreference,
194
+ '[data-font-preference-switch]',
195
+ fontPreference === 'serif',
196
+ fontPreference === 'serif' ? 'Use sans-serif fonts' : 'Use serif fonts',
197
+ );
198
+ syncSwitch(
199
+ document,
200
+ '[data-contrast-preference-switch]',
201
+ contrastPreference === 'high',
202
+ contrastPreference === 'high'
203
+ ? 'Use standard contrast'
204
+ : 'Use high contrast',
168
205
  );
169
206
  }
170
207
 
@@ -179,29 +216,82 @@ export default function mountAppearancePicker(window: Window): () => void {
179
216
  container.querySelectorAll<HTMLButtonElement>('button'),
180
217
  );
181
218
  const storage = getStorage(window);
219
+ const fontLoader = getFontPreferenceLoader(window);
182
220
  const defaultFontPreference = getDefaultFontPreference(document);
183
221
  const defaultContrastPreference = getDefaultContrastPreference(window);
184
- let fontPreference = getFontPreference(storage, defaultFontPreference);
222
+ const requestedFontPreference = getFontPreference(
223
+ storage,
224
+ defaultFontPreference,
225
+ );
226
+ let fontPreference = getAppliedFontPreference(document);
185
227
  let contrastPreference = getContrastPreference(
186
228
  storage,
187
229
  defaultContrastPreference,
188
230
  );
189
231
 
190
- applyFontPreference(document, fontPreference);
232
+ const requestedFontIsWaiting =
233
+ (fontLoader?.pending?.preference === requestedFontPreference ||
234
+ fontLoader?.failedPreference === requestedFontPreference) ??
235
+ false;
236
+ if (!requestedFontIsWaiting) {
237
+ fontPreference = requestedFontPreference;
238
+ applyFontPreference(document, fontPreference);
239
+ }
191
240
  applyContrastPreference(document, contrastPreference);
192
241
  syncAppearance(document, fontPreference, contrastPreference);
193
242
 
243
+ const observedRequests = new Set<number>();
244
+ const observeFontRequest = (request: FontPreferenceRequest) => {
245
+ if (observedRequests.has(request.generation)) {
246
+ return;
247
+ }
248
+ observedRequests.add(request.generation);
249
+ request.promise.then(
250
+ () => {
251
+ if (fontLoader?.isCurrent(request)) {
252
+ fontPreference = request.preference;
253
+ saveFontPreference(storage, fontPreference, defaultFontPreference);
254
+ applyFontPreference(document, fontPreference);
255
+ fontLoader.complete(request);
256
+ } else {
257
+ fontPreference = getAppliedFontPreference(document);
258
+ }
259
+ syncAppearance(document, fontPreference, contrastPreference);
260
+ },
261
+ () => {
262
+ fontLoader?.fail(request);
263
+ fontPreference = getAppliedFontPreference(document);
264
+ syncAppearance(document, fontPreference, contrastPreference);
265
+ },
266
+ );
267
+ };
268
+
269
+ if (fontLoader?.pending) {
270
+ observeFontRequest(fontLoader.pending);
271
+ }
272
+
194
273
  const handleClick = (event: Event) => {
195
274
  const button = event.currentTarget as HTMLButtonElement;
196
- const fontValue = button.dataset.fontPreferenceValue;
197
- const contrastValue = button.dataset.contrastPreferenceValue;
198
275
 
199
- if (fontValue === 'sans' || fontValue === 'serif') {
200
- fontPreference = fontValue;
276
+ if (button.dataset.fontPreferenceSwitch !== undefined) {
277
+ fontPreference = getAppliedFontPreference(document);
278
+ if (fontLoader?.pending) {
279
+ fontLoader?.cancel();
280
+ saveFontPreference(storage, fontPreference, defaultFontPreference);
281
+ syncAppearance(document, fontPreference, contrastPreference);
282
+ return;
283
+ }
284
+ const nextFontPreference = fontPreference === 'serif' ? 'sans' : 'serif';
285
+ const request = fontLoader?.request(nextFontPreference) ?? null;
286
+ if (request) {
287
+ observeFontRequest(request);
288
+ return;
289
+ }
290
+ fontPreference = nextFontPreference;
201
291
  saveFontPreference(storage, fontPreference, defaultFontPreference);
202
292
  applyFontPreference(document, fontPreference);
203
- } else if (contrastValue === 'standard' || contrastValue === 'high') {
204
- contrastPreference = contrastValue;
293
+ } else if (button.dataset.contrastPreferenceSwitch !== undefined) {
294
+ contrastPreference = contrastPreference === 'high' ? 'standard' : 'high';
205
295
  saveContrastPreference(
206
296
  storage,
207
297
  contrastPreference,
@@ -21,6 +21,7 @@
21
21
  width: fit-content;
22
22
  padding: var(--preference-picker-padding);
23
23
  background: var(--bg2-color);
24
+ border: 0;
24
25
  border-radius: calc(var(--border-radius) + 4px);
25
26
  }
26
27
 
@@ -46,15 +47,15 @@
46
47
  transform: translateX(calc(100% + var(--preference-picker-gap)));
47
48
  }
48
49
 
49
- .preference-picker:has(
50
- .preference-picker-option[aria-pressed='true']:is(:hover, :focus)
51
- )::before {
50
+ .preference-picker:is(:hover, :focus)::before {
52
51
  background: var(--bg2-color);
53
52
  }
54
53
 
55
- .preference-picker:has(
56
- .preference-picker-option[aria-pressed='true']:active:not(:disabled)
57
- )::before {
54
+ .preference-picker:active:not(:disabled) {
55
+ background: var(--bg2-color) !important;
56
+ }
57
+
58
+ .preference-picker:active:not(:disabled)::before {
58
59
  background: var(--link-active-color);
59
60
  }
60
61
 
@@ -65,9 +66,8 @@
65
66
  align-items: center;
66
67
  justify-content: center;
67
68
  width: var(--preference-picker-option-width);
68
- height: 2rem;
69
+ height: 1.8rem;
69
70
  padding: 0;
70
- font-size: var(--font-size-smaller);
71
71
  color: var(--fg2-color);
72
72
  background: transparent;
73
73
  border: 0;
@@ -100,17 +100,22 @@
100
100
  font-size-adjust: var(--font-size-adjust);
101
101
  }
102
102
 
103
- .preference-picker-option[aria-pressed='true'] {
103
+ :root:not([data-font-preference='serif']) .font-picker-option-sans,
104
+ :root[data-font-preference='serif'] .font-picker-option-serif,
105
+ :root:not([data-contrast-preference='high']) .contrast-picker-option-standard,
106
+ :root[data-contrast-preference='high'] .contrast-picker-option-high {
104
107
  color: var(--fg-color);
105
- background: transparent;
106
108
  }
107
109
 
108
- .preference-picker-option[aria-pressed='true']:is(:hover, :focus) {
109
- background: transparent;
110
- }
111
-
112
- .preference-picker-option[aria-pressed='true']:active:not(:disabled) {
113
- background: transparent !important;
110
+ .preference-picker:active:not(:disabled) {
111
+ :root:not([data-font-preference='serif']) & .font-picker-option-sans,
112
+ :root[data-font-preference='serif'] & .font-picker-option-serif,
113
+ :root:not([data-contrast-preference='high'])
114
+ &
115
+ .contrast-picker-option-standard,
116
+ :root[data-contrast-preference='high'] & .contrast-picker-option-high {
117
+ color: var(--bg-color);
118
+ }
114
119
  }
115
120
 
116
121
  @media print {
@@ -1,8 +1,10 @@
1
1
  # The `header` component
2
2
 
3
- Use the [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API)
4
- to animate the `<details>` element in the header.
3
+ CSS morphs the header's three-stroke inline SVG between menu and close states
4
+ and, where supported, animates its `<details>` content. The component's
5
+ JavaScript adds dismissal behavior for outside clicks, focus leaving the menu,
6
+ and the Escape key.
5
7
 
6
8
  This component doesn't provide any other functionality related to the header.
7
9
  If JavaScript is turned off in the web browser, the `<details>` element is still
8
- functional but with no animation.
10
+ functional and its disclosure content opens immediately.
@@ -2,6 +2,7 @@
2
2
 
3
3
  :root {
4
4
  --header-collapsed-height: 45px;
5
+ --header-menu-icon-size: 24px;
5
6
  }
6
7
 
7
8
  header {
@@ -75,21 +76,77 @@ header details summary::marker {
75
76
  content: '';
76
77
  }
77
78
 
78
- header summary::after {
79
- flex: 0 0 var(--icon-header-menu-size);
79
+ header summary .menu-icon {
80
+ flex: 0 0 var(--header-menu-icon-size);
80
81
  order: -1;
81
- width: var(--icon-header-menu-size);
82
- height: var(--icon-header-menu-size);
82
+ width: var(--header-menu-icon-size);
83
+ height: var(--header-menu-icon-size);
83
84
  margin-right: 8px;
84
85
  margin-left: var(--gap);
85
- content: '';
86
+ overflow: visible;
87
+ pointer-events: none;
88
+ }
89
+
90
+ header summary .menu-icon-line {
91
+ transform-origin: center;
92
+ transform-box: fill-box;
93
+ }
94
+
95
+ @media (prefers-reduced-motion: no-preference) {
96
+ header summary .menu-icon-line {
97
+ transition: transform 200ms cubic-bezier(0.2, 0, 0, 1);
98
+ }
99
+
100
+ header summary .menu-icon-line-middle {
101
+ transition:
102
+ opacity 150ms ease-out,
103
+ transform 200ms cubic-bezier(0.2, 0, 0, 1);
104
+ }
105
+ }
106
+
107
+ header details[open] summary .menu-icon-line-top {
108
+ transform: translateY(6px) rotate(45deg);
109
+ }
110
+
111
+ header details[open] summary .menu-icon-line-middle {
112
+ opacity: 0;
113
+ transform: scaleX(0.8);
114
+ }
86
115
 
87
- @include contained-mask(var(--icon-header-menu));
116
+ header details[open] summary .menu-icon-line-bottom {
117
+ transform: translateY(-6px) rotate(-45deg);
88
118
  }
89
119
 
90
- header details[open] summary::after {
91
- -webkit-mask-image: var(--icon-header-close);
92
- mask-image: var(--icon-header-close);
120
+ @supports selector(details::details-content) and
121
+ (transition-behavior: allow-discrete) {
122
+ @media (prefers-reduced-motion: no-preference) {
123
+ :root.js header > details {
124
+ display: grid;
125
+ grid-template-rows: var(--header-collapsed-height) 0fr;
126
+ grid-template-columns: minmax(0, 1fr);
127
+ transition: grid-template-rows 200ms cubic-bezier(0.2, 0, 0, 1);
128
+ }
129
+
130
+ :root.js header > details[open] {
131
+ grid-template-rows: var(--header-collapsed-height) 1fr;
132
+ }
133
+
134
+ :root.js header > details::details-content {
135
+ min-block-size: 0;
136
+ overflow: clip;
137
+ opacity: 0;
138
+ transform: translateY(-4px);
139
+ transition:
140
+ content-visibility 200ms allow-discrete,
141
+ opacity 150ms ease-out,
142
+ transform 200ms cubic-bezier(0.2, 0, 0, 1);
143
+ }
144
+
145
+ :root.js header > details[open]::details-content {
146
+ opacity: 1;
147
+ transform: none;
148
+ }
149
+ }
93
150
  }
94
151
 
95
152
  header .content {
@@ -14,6 +14,11 @@ header {
14
14
  .results-container.is-showing .results {
15
15
  pointer-events: all;
16
16
  opacity: 1;
17
+ transform: none;
18
+ }
19
+
20
+ &.loading .results {
21
+ transition: none;
17
22
  }
18
23
 
19
24
  @media (hover: hover) {
@@ -37,6 +42,14 @@ header {
37
42
  border-radius: var(--border-radius);
38
43
  box-shadow: var(--box-shadow);
39
44
  opacity: 0;
45
+ transform: translateY(-4px) scale(0.98);
46
+ transform-origin: top center;
47
+
48
+ @media (prefers-reduced-motion: no-preference) {
49
+ transition:
50
+ opacity 150ms ease-out,
51
+ transform 200ms cubic-bezier(0.2, 0, 0, 1);
52
+ }
40
53
 
41
54
  @include user-select(none);
42
55
 
@@ -1,20 +1,16 @@
1
1
  <% if (site.features.pickers) { %>
2
2
  <div class="appearance-pickers" data-pagefind-ignore>
3
- <div class="preference-picker font-picker" role="group" aria-label="Font style">
4
- <button type="button" class="preference-picker-option font-picker-option-sans" data-font-preference-value="sans" aria-label="Use sans-serif fonts" aria-pressed="<%= site.defaultFont === 'sans' %>" disabled>
5
- <span aria-hidden="true">Aa</span>
6
- </button>
7
- <button type="button" class="preference-picker-option font-picker-option-serif" data-font-preference-value="serif" aria-label="Use serif fonts" aria-pressed="<%= site.defaultFont === 'serif' %>" disabled>
8
- <span aria-hidden="true">Aa</span>
9
- </button>
10
- </div>
11
- <div class="preference-picker contrast-picker" role="group" aria-label="Contrast">
12
- <button type="button" class="preference-picker-option contrast-picker-option-standard" data-contrast-preference-value="standard" aria-label="Use standard contrast" aria-pressed="<%= site.defaultContrast === 'standard' %>" disabled>
13
- <span class="material-symbol-icon material-symbol-icon-contrast-standard" aria-hidden="true"></span>
14
- </button>
15
- <button type="button" class="preference-picker-option contrast-picker-option-high" data-contrast-preference-value="high" aria-label="Use high contrast" aria-pressed="<%= site.defaultContrast === 'high' %>" disabled>
16
- <span class="material-symbol-icon material-symbol-icon-contrast-high" aria-hidden="true"></span>
17
- </button>
18
- </div>
3
+ <button type="button" class="preference-picker font-picker" role="switch" data-font-preference-switch aria-label="Use serif fonts" aria-checked="<%= site.defaultFont === 'serif' %>" title="<%= site.defaultFont === 'serif' ? 'Use sans-serif fonts' : 'Use serif fonts' %>" disabled>
4
+ <span class="preference-picker-option font-picker-option-sans" data-font-preference-value="sans" aria-hidden="true">Aa</span>
5
+ <span class="preference-picker-option font-picker-option-serif" data-font-preference-value="serif" aria-hidden="true">Aa</span>
6
+ </button>
7
+ <button type="button" class="preference-picker contrast-picker" role="switch" data-contrast-preference-switch aria-label="Use high contrast" aria-checked="<%= site.defaultContrast === 'high' %>" title="<%= site.defaultContrast === 'high' ? 'Use standard contrast' : 'Use high contrast' %>" disabled>
8
+ <span class="preference-picker-option contrast-picker-option-standard" data-contrast-preference-value="standard" aria-hidden="true">
9
+ <span class="material-symbol-icon material-symbol-icon-contrast-standard"></span>
10
+ </span>
11
+ <span class="preference-picker-option contrast-picker-option-high" data-contrast-preference-value="high" aria-hidden="true">
12
+ <span class="material-symbol-icon material-symbol-icon-contrast-high"></span>
13
+ </span>
14
+ </button>
19
15
  </div>
20
16
  <% } %>
@@ -38,7 +38,70 @@
38
38
  <% } %>
39
39
  <title><%= page.title %><%= site.titlePostfix %></title>
40
40
  <script>
41
+ (() => {
41
42
  document.documentElement.classList.add('js');
43
+ const fontPreferenceFamilies = {
44
+ sans: ['Inter', 'Google Sans Code'],
45
+ serif: [
46
+ '<%= site.fontOverrides?.serif ? 'Tada Custom Serif' : 'Source Serif 4' %>',
47
+ '<%= site.fontOverrides?.serifMono ? 'Tada Custom Serif Mono' : 'Libertinus Mono' %>',
48
+ ],
49
+ };
50
+ const fontPreferenceLoader = {
51
+ generation: 0,
52
+ pending: null,
53
+ failedPreference: null,
54
+ supported:
55
+ document.fonts && typeof document.fonts.load === 'function',
56
+ request(preference) {
57
+ if (!this.supported) return null;
58
+ if (this.pending?.preference === preference) return this.pending;
59
+
60
+ const generation = ++this.generation;
61
+ const [bodyFamily, monoFamily] = fontPreferenceFamilies[preference];
62
+ const promise = Promise.all([
63
+ document.fonts.load('normal 400 1em "' + bodyFamily + '"'),
64
+ document.fonts.load('normal 700 1em "' + bodyFamily + '"'),
65
+ document.fonts.load('normal 400 1em "' + monoFamily + '"'),
66
+ ]).then(matches => {
67
+ if (matches.some(faces => faces.length === 0)) {
68
+ throw new Error('A required font face did not load');
69
+ }
70
+ });
71
+ const request = { generation, preference, promise };
72
+ this.pending = request;
73
+ this.failedPreference = null;
74
+ return request;
75
+ },
76
+ isCurrent(request) {
77
+ return (
78
+ this.pending === request &&
79
+ this.generation === request.generation
80
+ );
81
+ },
82
+ complete(request) {
83
+ if (this.isCurrent(request)) this.pending = null;
84
+ },
85
+ fail(request) {
86
+ if (!this.isCurrent(request)) return;
87
+ this.pending = null;
88
+ this.failedPreference = request.preference;
89
+ },
90
+ cancel() {
91
+ ++this.generation;
92
+ this.pending = null;
93
+ this.failedPreference = null;
94
+ },
95
+ };
96
+ window.__tadaFontPreferenceLoader = fontPreferenceLoader;
97
+
98
+ const applyFontPreference = preference => {
99
+ if (preference === 'serif') {
100
+ document.documentElement.dataset.fontPreference = 'serif';
101
+ } else {
102
+ delete document.documentElement.dataset.fontPreference;
103
+ }
104
+ };
42
105
  try {
43
106
  const preference = window.localStorage.getItem('fontPreference');
44
107
  const configured =
@@ -47,10 +110,27 @@
47
110
  (preference === 'serif' || preference === 'sans') &&
48
111
  preference !== configured
49
112
  ) {
50
- if (preference === 'serif') {
51
- document.documentElement.dataset.fontPreference = 'serif';
113
+ const request = fontPreferenceLoader.request(preference);
114
+ if (!request) {
115
+ applyFontPreference(preference);
52
116
  } else {
53
- delete document.documentElement.dataset.fontPreference;
117
+ request.promise.then(
118
+ () => {
119
+ let storedPreference;
120
+ try {
121
+ storedPreference =
122
+ window.localStorage.getItem('fontPreference');
123
+ } catch {}
124
+ if (
125
+ fontPreferenceLoader.isCurrent(request) &&
126
+ storedPreference === preference
127
+ ) {
128
+ applyFontPreference(preference);
129
+ fontPreferenceLoader.complete(request);
130
+ }
131
+ },
132
+ () => fontPreferenceLoader.fail(request),
133
+ );
54
134
  }
55
135
  }
56
136
  } catch {}
@@ -81,6 +161,7 @@
81
161
  } else {
82
162
  delete document.documentElement.dataset.contrastPreference;
83
163
  }
164
+ })();
84
165
  </script>
85
166
  <% if (!isWatchMode) { %>
86
167
  <script type="speculationrules">
@@ -127,6 +208,12 @@
127
208
  </div>
128
209
  <details>
129
210
  <summary aria-label="Toggle site navigation">
211
+ <svg class="menu-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"
212
+ fill="none" stroke="currentColor" stroke-width="1.65" stroke-linecap="round">
213
+ <path class="menu-icon-line menu-icon-line-top" d="M3 6h18" />
214
+ <path class="menu-icon-line menu-icon-line-middle" d="M3 12h18" />
215
+ <path class="menu-icon-line menu-icon-line-bottom" d="M3 18h18" />
216
+ </svg>
130
217
  <% if (site.symbol) { %><span class="logo" aria-hidden="true"><%= site.symbol %></span><% } %>
131
218
  <span class="site-title" aria-hidden="true"><%= site.title %></span>
132
219
  </summary>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M252-203.69 205.69-252l227-228-227-230L252-758.31l229 230 227-230L754.31-710l-227 230 227 228L708-203.69l-227-230-229 230Z"/></svg>
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M128-251.23v-66h704v66H128ZM128-448v-66h704v66H128Zm0-196.77v-66h704v66H128Z"/></svg>