@ckeditor/ckeditor5-dev-manual-server 57.4.0 → 59.0.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/theme/catalog.css CHANGED
@@ -1,3 +1,8 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md.
4
+ */
5
+
1
6
  :root {
2
7
  color-scheme: light;
3
8
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
@@ -15,6 +20,10 @@
15
20
  --text-subtle: #a3a3a3;
16
21
  --accent: #171717;
17
22
  --accent-ring: rgb(23 23 23 / 10%);
23
+ --search-match-background: #e6d3fa;
24
+ --search-match-text: #480090;
25
+ --favorites-surface: #f7f0ff;
26
+ --favorites-border: #d9c2f0;
18
27
  --radius-lg: 12px;
19
28
  --radius-md: 8px;
20
29
  --radius-sm: 6px;
@@ -36,6 +45,12 @@ a {
36
45
  color: inherit;
37
46
  }
38
47
 
48
+ ::highlight(ckeditor5-manual-search-match) {
49
+ background-color: var(--search-match-background);
50
+ color: var(--search-match-text);
51
+ }
52
+
53
+ /* Application shell. */
39
54
  .app {
40
55
  max-width: 1280px;
41
56
  margin: 0 auto;
@@ -75,12 +90,11 @@ a {
75
90
  font-variant-numeric: tabular-nums;
76
91
  }
77
92
 
93
+ /* Search. */
78
94
  .app__search {
79
95
  display: flex;
80
96
  align-items: center;
81
- gap: 10px;
82
97
  height: 44px;
83
- padding: 0 14px;
84
98
  background: var(--surface);
85
99
  border: 1px solid var(--border);
86
100
  border-radius: var(--radius-md);
@@ -92,7 +106,19 @@ a {
92
106
  box-shadow: 0 0 0 4px var(--accent-ring);
93
107
  }
94
108
 
95
- .app__search-icon {
109
+ .app__search-label {
110
+ display: flex;
111
+ align-items: center;
112
+ align-self: stretch;
113
+ flex: 1;
114
+ min-width: 0;
115
+ gap: 10px;
116
+ padding: 0 10px 0 14px;
117
+ cursor: text;
118
+ }
119
+
120
+ .app__search-icon,
121
+ .app__search-clear {
96
122
  flex-shrink: 0;
97
123
  color: var(--text-subtle);
98
124
  }
@@ -110,13 +136,33 @@ a {
110
136
  }
111
137
 
112
138
  .app__search-input::-webkit-search-cancel-button {
113
- cursor: pointer;
139
+ display: none;
114
140
  }
115
141
 
116
142
  .app__search-input::placeholder {
117
143
  color: var(--text-muted);
118
144
  }
119
145
 
146
+ .app__search:has(.app__search-input:placeholder-shown) .app__search-clear {
147
+ visibility: hidden;
148
+ }
149
+
150
+ .app__search-clear {
151
+ margin-inline-end: 10px;
152
+ padding: 6px;
153
+ line-height: 0;
154
+ background: none;
155
+ border: 0;
156
+ border-radius: var(--radius-sm);
157
+ cursor: pointer;
158
+ }
159
+
160
+ .app__search-clear:hover {
161
+ color: var(--text);
162
+ background: var(--surface-hover);
163
+ }
164
+
165
+ /* Catalog content. */
120
166
  .app__list {
121
167
  display: grid;
122
168
  grid-template-columns: repeat(auto-fill, minmax(360px, 1fr));
@@ -145,6 +191,7 @@ a {
145
191
  color: var(--text-muted);
146
192
  }
147
193
 
194
+ /* Package cards. */
148
195
  .pkg {
149
196
  background: var(--surface);
150
197
  border: 1px solid var(--border);
@@ -160,11 +207,14 @@ a {
160
207
  .pkg__header {
161
208
  display: flex;
162
209
  align-items: center;
163
- gap: 10px;
164
- padding: 12px 16px;
210
+ padding: 12px 6px 12px 16px;
165
211
  border-bottom: 1px solid var(--border);
166
212
  }
167
213
 
214
+ .pkg__header > * + * {
215
+ margin-inline-start: 10px;
216
+ }
217
+
168
218
  .pkg__name {
169
219
  flex: 1;
170
220
  min-width: 0;
@@ -177,23 +227,91 @@ a {
177
227
  }
178
228
 
179
229
  .pkg__count {
180
- flex-shrink: 0;
181
- min-width: 24px;
182
- text-align: right;
230
+ flex: 0 0 38px;
231
+ text-align: center;
183
232
  font-size: 12px;
184
233
  color: var(--text-muted);
185
234
  font-variant-numeric: tabular-nums;
186
235
  }
187
236
 
237
+ .pkg__favorite,
238
+ .pkg__favorites-clear {
239
+ flex-shrink: 0;
240
+ line-height: 0;
241
+ color: var(--text-subtle);
242
+ background: none;
243
+ border: 0;
244
+ border-radius: var(--radius-sm);
245
+ cursor: pointer;
246
+ }
247
+
248
+ /* Favorites card. */
249
+ .pkg--favorites,
250
+ .pkg--favorites:hover {
251
+ border-color: var(--favorites-border);
252
+ }
253
+
254
+ .pkg--favorites .pkg__header {
255
+ color: var(--search-match-text);
256
+ background: var(--favorites-surface);
257
+ }
258
+
259
+ .pkg--favorites .pkg__name {
260
+ font-family: inherit;
261
+ }
262
+
263
+ .pkg__favorites-icon {
264
+ flex-shrink: 0;
265
+ }
266
+
267
+ .pkg__favorites-icon path,
268
+ .pkg__favorite-icon path {
269
+ fill: none;
270
+ stroke: currentColor;
271
+ stroke-linejoin: round;
272
+ stroke-width: 1.5px;
273
+ }
274
+
275
+ .pkg__favorites-icon path,
276
+ .favorite--active .pkg__favorite-icon path {
277
+ fill: currentColor;
278
+ }
279
+
280
+ .pkg__favorites-clear {
281
+ padding: 0;
282
+ }
283
+
284
+ .pkg__favorites-clear:hover {
285
+ color: var(--text);
286
+ }
287
+
288
+ .pkg__favorites-clear:not([hidden]) + .pkg__count {
289
+ margin-inline-start: 2px;
290
+ }
291
+
292
+ /* Package contents. */
188
293
  .pkg__list {
189
294
  list-style: none;
190
295
  margin: 0;
191
296
  padding: 6px;
192
297
  }
193
298
 
299
+ .pkg__favorites-empty {
300
+ padding: 10px 12px;
301
+ font-size: 12px;
302
+ color: var(--text-muted);
303
+ }
304
+
305
+ .pkg__item {
306
+ display: flex;
307
+ align-items: center;
308
+ }
309
+
194
310
  .pkg__link {
195
311
  display: flex;
196
312
  align-items: center;
313
+ flex: 1;
314
+ min-width: 0;
197
315
  gap: 12px;
198
316
  padding: 10px 12px;
199
317
  border-radius: var(--radius-sm);
@@ -206,12 +324,6 @@ a {
206
324
  background: var(--surface-hover);
207
325
  }
208
326
 
209
- .pkg__link:focus-visible {
210
- outline: none;
211
- background: var(--surface-hover);
212
- box-shadow: inset 0 0 0 2px var(--border-strong);
213
- }
214
-
215
327
  .pkg__test-name {
216
328
  flex: 1;
217
329
  min-width: 0;
@@ -223,6 +335,27 @@ a {
223
335
  white-space: nowrap;
224
336
  }
225
337
 
338
+ .pkg__favorite {
339
+ padding: 11px;
340
+ }
341
+
342
+ .pkg__favorite:hover {
343
+ color: var(--text);
344
+ background: var(--surface-hover);
345
+ }
346
+
347
+ .pkg__favorite.favorite--active {
348
+ color: var(--search-match-text);
349
+ }
350
+
351
+ .pkg__link:focus-visible,
352
+ .pkg__favorite:focus-visible,
353
+ .pkg__favorites-clear:focus-visible {
354
+ outline: none;
355
+ background: var(--surface-hover);
356
+ box-shadow: inset 0 0 0 2px var(--border-strong);
357
+ }
358
+
226
359
  @media (max-width: 600px) {
227
360
  .app {
228
361
  padding: 0 16px 48px;
@@ -15,12 +15,19 @@
15
15
  <h1 class="app__title">CKEditor 5 manual tests</h1>
16
16
  <p class="app__subtitle"></p>
17
17
  </div>
18
- <label class="app__search">
19
- <svg class="app__search-icon" viewBox="0 0 16 16" aria-hidden="true" width="16" height="16">
20
- <path fill="currentColor" d="M7 2a5 5 0 103.546 8.546l3.204 3.204a.75.75 0 101.06-1.06l-3.204-3.204A5 5 0 007 2zM3.5 7a3.5 3.5 0 117 0 3.5 3.5 0 01-7 0z"/>
21
- </svg>
22
- <input type="search" class="app__search-input" placeholder="Filter by package or test name…" autocomplete="off" autofocus>
23
- </label>
18
+ <div class="app__search" role="search">
19
+ <label class="app__search-label">
20
+ <svg class="app__search-icon" viewBox="0 0 16 16" aria-hidden="true" width="16" height="16">
21
+ <path fill="currentColor" d="M7 2a5 5 0 103.546 8.546l3.204 3.204a.75.75 0 101.06-1.06l-3.204-3.204A5 5 0 007 2zM3.5 7a3.5 3.5 0 117 0 3.5 3.5 0 01-7 0z"/>
22
+ </svg>
23
+ <input type="search" class="app__search-input" placeholder="Filter by package or test name…" autocomplete="off" autofocus>
24
+ </label>
25
+ <button type="button" class="app__search-clear" aria-label="Clear search">
26
+ <svg viewBox="0 0 16 16" aria-hidden="true" width="16" height="16">
27
+ <path fill="currentColor" d="M3.22 3.22a.75.75 0 011.06 0L8 6.94l3.72-3.72a.75.75 0 111.06 1.06L9.06 8l3.72 3.72a.75.75 0 11-1.06 1.06L8 9.06l-3.72 3.72a.75.75 0 01-1.06-1.06L6.94 8 3.22 4.28a.75.75 0 010-1.06z"/>
28
+ </svg>
29
+ </button>
30
+ </div>
24
31
  </header>
25
32
  <div class="app__list" role="list"></div>
26
33
  </main>
@@ -43,11 +50,31 @@
43
50
  </section>
44
51
  </template>
45
52
 
53
+ <template id="manual-favorites-template">
54
+ <section class="pkg pkg--favorites" role="listitem">
55
+ <header class="pkg__header">
56
+ <span class="pkg__name">Favorites</span>
57
+ <button type="button" class="pkg__favorites-clear" aria-label="Clear all favorites" title="Clear all favorites" hidden>
58
+ <svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
59
+ <path fill="currentColor" d="M5.5 2.5h5l.5 1H14V5H2V3.5h3l.5-1zM3.5 6h9l-.65 8h-7.7L3.5 6zm2.25 1.5v5h1.5v-5h-1.5zm3 0v5h1.5v-5h-1.5z"/>
60
+ </svg>
61
+ </button>
62
+ <span class="pkg__count"></span>
63
+ </header>
64
+ <ul class="pkg__list"></ul>
65
+ </section>
66
+ </template>
67
+
68
+ <template id="manual-favorites-empty-template">
69
+ <li class="pkg__favorites-empty">Star tests to add them here.</li>
70
+ </template>
71
+
46
72
  <template id="manual-test-template">
47
73
  <li class="pkg__item">
48
74
  <a class="pkg__link">
49
75
  <span class="pkg__test-name"></span>
50
76
  </a>
77
+ <button type="button" class="pkg__favorite"></button>
51
78
  </li>
52
79
  </template>
53
80
 
package/theme/catalog.ts CHANGED
@@ -5,10 +5,22 @@
5
5
 
6
6
  // @ts-expect-error Virtual module provided by the manual test entries plugin.
7
7
  import { manualTestEntries, type ManualTestEntry } from 'virtual:ckeditor5-manual-entries';
8
+ import {
9
+ filterFavoriteEntries,
10
+ getFavoriteId,
11
+ loadFavoriteIds,
12
+ saveFavoriteIds,
13
+ toggleFavoriteId,
14
+ updateFavoriteButtonState
15
+ } from './catalog-favorites.js';
16
+ import { filterEntries, findMatchOffsets } from './catalog-search.js';
17
+ import { createFavoriteIcon } from './favorite-icon.js';
8
18
 
9
19
  import './catalog.css';
10
20
 
11
21
  const SEARCH_QUERY_PARAMETER = 'q';
22
+ const SEARCH_HIGHLIGHT_NAME = 'ckeditor5-manual-search-match';
23
+ const SEARCH_LABEL_SELECTOR = '.pkg__name, .pkg__test-name';
12
24
 
13
25
  /**
14
26
  * Root element for the manual test index application.
@@ -22,6 +34,7 @@ renderApp( appElement, manualTestEntries );
22
34
  */
23
35
  function renderApp( root: HTMLElement, entries: Array<ManualTestEntry> ): void {
24
36
  const packageCount = new Set( entries.map( entry => entry.packageName ) ).size;
37
+ const favoriteIds = loadFavoriteIds();
25
38
 
26
39
  root.replaceChildren( cloneTemplateElement( 'manual-index-template' ) );
27
40
 
@@ -29,9 +42,18 @@ function renderApp( root: HTMLElement, entries: Array<ManualTestEntry> ): void {
29
42
 
30
43
  const listElement = root.querySelector<HTMLElement>( '.app__list' )!;
31
44
  const searchInput = root.querySelector<HTMLInputElement>( '.app__search-input' )!;
45
+ const clearSearchButton = root.querySelector<HTMLButtonElement>( '.app__search-clear' )!;
32
46
 
33
47
  const render = ( query: string ): void => {
34
- listElement.replaceChildren( ...renderGroups( filterEntries( entries, query ) ) );
48
+ const normalizedQuery = query.trim().toLowerCase();
49
+ const renderedGroups = renderGroups( filterEntries( entries, normalizedQuery ), favoriteIds );
50
+
51
+ if ( !normalizedQuery && entries.length ) {
52
+ renderedGroups.unshift( renderFavoritesCard( entries, favoriteIds ) );
53
+ }
54
+
55
+ listElement.replaceChildren( ...renderedGroups );
56
+ updateSearchHighlights( listElement, normalizedQuery );
35
57
  };
36
58
 
37
59
  const setQuery = ( query: string ): void => {
@@ -39,9 +61,54 @@ function renderApp( root: HTMLElement, entries: Array<ManualTestEntry> ): void {
39
61
  render( query );
40
62
  };
41
63
 
42
- searchInput.addEventListener( 'input', () => {
64
+ const applySearchInput = (): void => {
43
65
  updateSearchQueryInUrl( searchInput.value );
44
66
  render( searchInput.value );
67
+ };
68
+
69
+ searchInput.addEventListener( 'input', applySearchInput );
70
+
71
+ listElement.addEventListener( 'click', event => {
72
+ const target = event.target;
73
+
74
+ if ( !( target instanceof Element ) ) {
75
+ return;
76
+ }
77
+
78
+ const favoriteActionButton = target.closest<HTMLButtonElement>( '.pkg__favorite, .pkg__favorites-clear' );
79
+
80
+ if ( !favoriteActionButton ) {
81
+ return;
82
+ }
83
+
84
+ const favoriteId = favoriteActionButton.dataset.favoriteId;
85
+ const shouldRestoreFocus = document.activeElement == favoriteActionButton;
86
+
87
+ if ( favoriteId ) {
88
+ toggleFavoriteId( favoriteIds, favoriteId );
89
+ } else {
90
+ favoriteIds.clear();
91
+ }
92
+
93
+ saveFavoriteIds( favoriteIds );
94
+ render( searchInput.value );
95
+
96
+ if ( !shouldRestoreFocus ) {
97
+ return;
98
+ }
99
+
100
+ if ( favoriteId ) {
101
+ focusFavoriteButton( listElement, favoriteId );
102
+ } else {
103
+ searchInput.focus();
104
+ }
105
+ } );
106
+
107
+ clearSearchButton.addEventListener( 'click', () => {
108
+ searchInput.value = '';
109
+ searchInput.focus();
110
+
111
+ applySearchInput();
45
112
  } );
46
113
 
47
114
  window.addEventListener( 'popstate', () => {
@@ -59,6 +126,16 @@ function renderApp( root: HTMLElement, entries: Array<ManualTestEntry> ): void {
59
126
  setQuery( getSearchQueryFromUrl() );
60
127
  }
61
128
 
129
+ /**
130
+ * Focuses the re-rendered favorite control in its package card.
131
+ */
132
+ function focusFavoriteButton( root: HTMLElement, favoriteId: string ): void {
133
+ const favoriteButton = [ ...root.querySelectorAll<HTMLButtonElement>( '.pkg:not(.pkg--favorites) .pkg__favorite' ) ]
134
+ .find( button => button.dataset.favoriteId == favoriteId );
135
+
136
+ favoriteButton?.focus();
137
+ }
138
+
62
139
  /**
63
140
  * Returns the current search query from the page URL.
64
141
  */
@@ -86,25 +163,46 @@ function updateSearchQueryInUrl( query: string ): void {
86
163
  }
87
164
 
88
165
  /**
89
- * Returns entries matching the current search query.
166
+ * Highlights query matches in the currently rendered package and test names.
90
167
  */
91
- function filterEntries( entries: Array<ManualTestEntry>, query: string ): Array<ManualTestEntry> {
92
- const normalized = query.trim().toLowerCase();
168
+ function updateSearchHighlights( root: HTMLElement, normalizedQuery: string ): void {
169
+ if ( !( 'highlights' in CSS ) ) {
170
+ return;
171
+ }
93
172
 
94
- if ( !normalized ) {
95
- return entries;
173
+ CSS.highlights.delete( SEARCH_HIGHLIGHT_NAME );
174
+
175
+ if ( !normalizedQuery ) {
176
+ return;
96
177
  }
97
178
 
98
- return entries.filter( entry =>
99
- entry.packageName.toLowerCase().includes( normalized ) ||
100
- entry.slug.toLowerCase().includes( normalized )
101
- );
179
+ const ranges: Array<Range> = [];
180
+
181
+ for ( const label of root.querySelectorAll<HTMLElement>( SEARCH_LABEL_SELECTOR ) ) {
182
+ const textNode = label.firstChild;
183
+
184
+ if ( !textNode || textNode.nodeType != Node.TEXT_NODE ) {
185
+ continue;
186
+ }
187
+
188
+ for ( const { start, end } of findMatchOffsets( textNode.textContent!, normalizedQuery ) ) {
189
+ const range = new Range();
190
+
191
+ range.setStart( textNode, start );
192
+ range.setEnd( textNode, end );
193
+ ranges.push( range );
194
+ }
195
+ }
196
+
197
+ if ( ranges.length ) {
198
+ CSS.highlights.set( SEARCH_HIGHLIGHT_NAME, new Highlight( ...ranges ) );
199
+ }
102
200
  }
103
201
 
104
202
  /**
105
203
  * Groups visible entries by package and renders package cards.
106
204
  */
107
- function renderGroups( entries: Array<ManualTestEntry> ): Array<HTMLElement> {
205
+ function renderGroups( entries: Array<ManualTestEntry>, favoriteIds: Set<string> ): Array<HTMLElement> {
108
206
  if ( !entries.length ) {
109
207
  return [ cloneTemplateElement( 'manual-empty-template' ) ];
110
208
  }
@@ -118,32 +216,64 @@ function renderGroups( entries: Array<ManualTestEntry> ): Array<HTMLElement> {
118
216
  groups.set( entry.packageName, group );
119
217
  }
120
218
 
121
- return [ ...groups.entries() ].map( ( [ packageName, packageEntries ] ) => renderPackageCard( packageName, packageEntries ) );
219
+ return [ ...groups.entries() ].map( ( [ packageName, packageEntries ] ) =>
220
+ renderPackageCard( packageName, packageEntries, favoriteIds )
221
+ );
122
222
  }
123
223
 
124
224
  /**
125
225
  * Renders a single package card with all tests belonging to that package.
126
226
  */
127
- function renderPackageCard( packageName: string, entries: Array<ManualTestEntry> ): HTMLElement {
227
+ function renderPackageCard( packageName: string, entries: Array<ManualTestEntry>, favoriteIds: Set<string> ): HTMLElement {
128
228
  const packageCard = cloneTemplateElement( 'manual-package-template' );
129
229
  const list = packageCard.querySelector<HTMLElement>( '.pkg__list' )!;
130
230
 
131
231
  packageCard.querySelector<HTMLElement>( '.pkg__name' )!.textContent = packageName;
132
232
  packageCard.querySelector<HTMLElement>( '.pkg__count' )!.textContent = String( entries.length );
133
- list.replaceChildren( ...entries.map( entry => renderTestItem( entry ) ) );
233
+ list.replaceChildren( ...entries.map( entry => renderTestItem( entry, favoriteIds ) ) );
134
234
 
135
235
  return packageCard;
136
236
  }
137
237
 
238
+ /**
239
+ * Renders the favorites card, including its empty state.
240
+ */
241
+ function renderFavoritesCard( entries: Array<ManualTestEntry>, favoriteIds: Set<string> ): HTMLElement {
242
+ const favoritesCard = cloneTemplateElement( 'manual-favorites-template' );
243
+ const favoriteEntries = filterFavoriteEntries( entries, favoriteIds );
244
+ const list = favoritesCard.querySelector<HTMLElement>( '.pkg__list' )!;
245
+ const clearButton = favoritesCard.querySelector<HTMLButtonElement>( '.pkg__favorites-clear' )!;
246
+
247
+ favoritesCard.querySelector<HTMLElement>( '.pkg__header' )!.prepend( createFavoriteIcon( 'pkg__favorites-icon' ) );
248
+ favoritesCard.querySelector<HTMLElement>( '.pkg__count' )!.textContent = String( favoriteEntries.length );
249
+ clearButton.hidden = !favoriteEntries.length;
250
+
251
+ if ( favoriteEntries.length ) {
252
+ list.replaceChildren( ...favoriteEntries.map( entry => renderTestItem( entry, favoriteIds, true ) ) );
253
+ } else {
254
+ list.replaceChildren( cloneTemplateElement( 'manual-favorites-empty-template' ) );
255
+ }
256
+
257
+ return favoritesCard;
258
+ }
259
+
138
260
  /**
139
261
  * Renders a single test link inside a package card.
140
262
  */
141
- function renderTestItem( entry: ManualTestEntry ): HTMLElement {
263
+ function renderTestItem( entry: ManualTestEntry, favoriteIds: Set<string>, showPackageName = false ): HTMLElement {
142
264
  const testItem = cloneTemplateElement( 'manual-test-template' );
143
265
  const link = testItem.querySelector<HTMLAnchorElement>( '.pkg__link' )!;
266
+ const favoriteButton = testItem.querySelector<HTMLButtonElement>( '.pkg__favorite' )!;
267
+ const favoriteId = getFavoriteId( entry );
144
268
 
145
269
  link.href = entry.href;
146
- testItem.querySelector<HTMLElement>( '.pkg__test-name' )!.textContent = entry.slug;
270
+
271
+ testItem.querySelector<HTMLElement>( '.pkg__test-name' )!.textContent = showPackageName ? favoriteId : entry.slug;
272
+
273
+ favoriteButton.dataset.favoriteId = favoriteId;
274
+ favoriteButton.append( createFavoriteIcon( 'pkg__favorite-icon' ) );
275
+
276
+ updateFavoriteButtonState( favoriteButton, favoriteIds, favoriteId );
147
277
 
148
278
  return testItem;
149
279
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
3
+ * For licensing, see LICENSE.md.
4
+ */
5
+
6
+ const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
7
+ const FAVORITE_ICON_PATH = 'M8 1.5l1.94 3.93 4.34.63-3.14 3.06.74 4.32L8 11.4l-3.88 2.04.74-4.32-3.14-3.06 4.34-.63L8 1.5z';
8
+
9
+ /**
10
+ * Creates the favorite star icon used by the catalog and manual test header.
11
+ */
12
+ export function createFavoriteIcon( className: string ): SVGSVGElement {
13
+ const icon = document.createElementNS( SVG_NAMESPACE, 'svg' );
14
+ const path = document.createElementNS( SVG_NAMESPACE, 'path' );
15
+
16
+ icon.classList.add( className );
17
+ icon.setAttribute( 'viewBox', '0 0 16 16' );
18
+ icon.setAttribute( 'width', '16' );
19
+ icon.setAttribute( 'height', '16' );
20
+ icon.setAttribute( 'aria-hidden', 'true' );
21
+ path.setAttribute( 'd', FAVORITE_ICON_PATH );
22
+ icon.append( path );
23
+
24
+ return icon;
25
+ }
@@ -7,7 +7,7 @@
7
7
 
8
8
  .refresh-prompt {
9
9
  position: fixed;
10
- top: calc( var( --ck-manual-header-height, 44px ) / 1.25 );
10
+ top: 16px;
11
11
  left: 50%;
12
12
  z-index: 100000;
13
13
  box-sizing: border-box;
@@ -20,14 +20,14 @@
20
20
  cursor: pointer;
21
21
  opacity: 0;
22
22
  pointer-events: none;
23
- transform: translate(-50%, calc( -100% - var( --ck-manual-header-height, 44px ) ) );
23
+ transform: translate(-50%, calc( -100% - 16px ) );
24
24
  transition: transform 0.18s ease-out, opacity 0.18s ease-out, background 0.1s;
25
25
  }
26
26
 
27
27
  .refresh-prompt--visible {
28
28
  opacity: 1;
29
29
  pointer-events: auto;
30
- transform: translate(-50%, -50%);
30
+ transform: translate(-50%, 0);
31
31
  }
32
32
 
33
33
  .refresh-prompt:hover {
@@ -7,15 +7,9 @@
7
7
 
8
8
  :host {
9
9
  display: block;
10
- --ck-manual-header-height: 44px;
11
10
  }
12
11
 
13
12
  .bar {
14
- position: fixed;
15
- top: 0;
16
- left: 0;
17
- right: 0;
18
- z-index: 99999;
19
13
  display: flex;
20
14
  align-items: stretch;
21
15
  box-sizing: border-box;
@@ -37,7 +31,7 @@
37
31
  padding: 8px 14px;
38
32
  border: 0;
39
33
  background: transparent;
40
- border-radius: 999px;
34
+ border-radius: 6px;
41
35
  text-decoration: none;
42
36
  color: inherit;
43
37
  font: inherit;
@@ -74,7 +68,7 @@
74
68
  align-items: center;
75
69
  flex: 1 1 auto;
76
70
  gap: 10px;
77
- padding: 8px 14px;
71
+ padding: 0 14px;
78
72
  overflow: hidden;
79
73
  min-width: 0;
80
74
  }
@@ -96,6 +90,29 @@
96
90
  white-space: nowrap;
97
91
  }
98
92
 
93
+ .favorite {
94
+ flex-shrink: 0;
95
+ justify-content: center;
96
+ padding: 8px;
97
+ margin-inline-start: -8px;
98
+ color: #737373;
99
+ }
100
+
101
+ .favorite__icon {
102
+ fill: none;
103
+ stroke: currentColor;
104
+ stroke-linejoin: round;
105
+ stroke-width: 1.5px;
106
+ }
107
+
108
+ .favorite--active {
109
+ color: #6d28d9;
110
+ }
111
+
112
+ .favorite--active .favorite__icon {
113
+ fill: currentColor;
114
+ }
115
+
99
116
  :host( :not( [ has-instructions ] ) ) .instructions-only {
100
117
  display: none;
101
118
  }
@@ -119,7 +136,7 @@
119
136
  --panel-transition-duration: 0.18s;
120
137
 
121
138
  position: fixed;
122
- top: var(--ck-manual-header-height);
139
+ top: 0;
123
140
  right: 0;
124
141
  bottom: 0;
125
142
  z-index: 99998;