@ckeditor/ckeditor5-dev-manual-server 58.0.0 → 59.1.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/dist/index.d.ts +0 -1
- package/dist/index.js +20 -137
- package/dist/manual-test-plugin/collect-pages.d.ts +1 -1
- package/package.json +1 -1
- package/theme/catalog-favorites.ts +94 -0
- package/theme/catalog-search.ts +152 -0
- package/theme/catalog.css +148 -15
- package/theme/catalog.html +33 -6
- package/theme/catalog.ts +147 -17
- package/theme/favorite-icon.ts +25 -0
- package/theme/manual-bootstrap.css +3 -3
- package/theme/manual-header.css +26 -9
- package/theme/manual-header.ts +36 -4
- package/theme/manual-instructions.css +11 -3
- package/dist/static-assets-plugin/plugin.d.ts +0 -10
- package/dist/static-assets-plugin/static-assets.d.ts +0 -9
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* For licensing, see LICENSE.md.
|
|
4
4
|
*/
|
|
5
5
|
export { manualTestsPlugin, type ManualTestsPluginOptions } from "./manual-test-plugin/plugin.js";
|
|
6
|
-
export { manualStaticAssetsPlugin, type ManualStaticAssetsPluginOptions } from "./static-assets-plugin/plugin.js";
|
|
7
6
|
export { ckDebugPlugin } from "./debug-plugin/plugin.js";
|
|
8
7
|
export { refreshPlugin } from "./refresh-plugin/plugin.js";
|
|
9
8
|
export { rawSvgPlugin } from "./svg-plugin/plugin.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, globSync, readFileSync, realpathSync } from "node:fs";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { basename,
|
|
3
|
+
import { basename, isAbsolute, posix, relative, resolve } from "node:path";
|
|
4
4
|
//#region src/utils.ts
|
|
5
5
|
/**
|
|
6
6
|
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
@@ -61,11 +61,11 @@ function stripLeadingSlash(value) {
|
|
|
61
61
|
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
62
62
|
* For licensing, see LICENSE.md.
|
|
63
63
|
*/
|
|
64
|
-
const MANUAL_TESTS_DIRECTORY$1 = "
|
|
65
|
-
const MANUAL_TEST_SUFFIX$
|
|
64
|
+
const MANUAL_TESTS_DIRECTORY$1 = "manual/";
|
|
65
|
+
const MANUAL_TEST_SUFFIX$1 = ".manual.html";
|
|
66
66
|
/**
|
|
67
67
|
* Discovers manual test pages. A manual test is a single full HTML document named
|
|
68
|
-
* `<slug>.manual.html` located under a package's `
|
|
68
|
+
* `<slug>.manual.html` located under a package's `manual/` directory. Plain `.html`
|
|
69
69
|
* files are treated as static fixtures and are never collected here.
|
|
70
70
|
*
|
|
71
71
|
* The `patterns` are package root globs, for example `packages/*`.
|
|
@@ -75,12 +75,12 @@ function collectManualPages(patterns, workspaceRoot) {
|
|
|
75
75
|
return new Map(manualPages);
|
|
76
76
|
}
|
|
77
77
|
function matchManualPageFiles(patterns, workspaceRoot) {
|
|
78
|
-
return patterns.map((pattern) => `${pattern.replace(/\/+$/, "")}/${MANUAL_TESTS_DIRECTORY$1}**/*${MANUAL_TEST_SUFFIX$
|
|
78
|
+
return patterns.map((pattern) => `${pattern.replace(/\/+$/, "")}/${MANUAL_TESTS_DIRECTORY$1}**/*${MANUAL_TEST_SUFFIX$1}`).flatMap((pattern) => globSync(pattern, { cwd: workspaceRoot }).map((match) => toPosixPath(match)));
|
|
79
79
|
}
|
|
80
80
|
function toManualPageEntry(relativeFilePath, workspaceRoot) {
|
|
81
81
|
const directoryStartIndex = relativeFilePath.startsWith(MANUAL_TESTS_DIRECTORY$1) ? 0 : relativeFilePath.indexOf(`/${MANUAL_TESTS_DIRECTORY$1}`) + 1;
|
|
82
82
|
const packagePath = relativeFilePath.slice(0, Math.max(directoryStartIndex - 1, 0));
|
|
83
|
-
const slugPath = relativeFilePath.slice(directoryStartIndex +
|
|
83
|
+
const slugPath = relativeFilePath.slice(directoryStartIndex + 7);
|
|
84
84
|
return {
|
|
85
85
|
htmlFilePath: toPublicSpecifier(relativeFilePath),
|
|
86
86
|
packageName: packagePath ? packagePath.slice(packagePath.lastIndexOf("/") + 1) : basename(workspaceRoot),
|
|
@@ -94,9 +94,9 @@ function toManualPageEntry(relativeFilePath, workspaceRoot) {
|
|
|
94
94
|
* For licensing, see LICENSE.md.
|
|
95
95
|
*/
|
|
96
96
|
const MANUAL_HEADER_ELEMENT = "ck-manual-header";
|
|
97
|
-
const MANUAL_TEST_SUFFIX
|
|
98
|
-
const MANUAL_TESTS_DIRECTORY = "/
|
|
99
|
-
const
|
|
97
|
+
const MANUAL_TEST_SUFFIX = ".manual.html";
|
|
98
|
+
const MANUAL_TESTS_DIRECTORY = "/manual/";
|
|
99
|
+
const THEME_ENTRY_FILE_PATHS = ["theme/index-editor.css", "theme/index-content.css"];
|
|
100
100
|
const MANUAL_ENTRIES_VIRTUAL_ID = "virtual:ckeditor5-manual-entries";
|
|
101
101
|
const MANUAL_THEME_ROOT = realpathSync(fileURLToPath(import.meta.resolve("@ckeditor/ckeditor5-dev-manual-server/theme")));
|
|
102
102
|
const MANUAL_CATALOG_FILE_PATH = resolve(MANUAL_THEME_ROOT, "catalog.html");
|
|
@@ -113,14 +113,14 @@ function manualTestsPlugin(options) {
|
|
|
113
113
|
return getManualPages().get(toPublicSpecifier(relative(workspaceRoot, filePath)));
|
|
114
114
|
};
|
|
115
115
|
const getManualPageEntryForScriptSpecifier = (scriptSpecifier) => {
|
|
116
|
-
const htmlSpecifier = scriptSpecifier.replace(/\.(?:js|ts)$/, MANUAL_TEST_SUFFIX
|
|
116
|
+
const htmlSpecifier = scriptSpecifier.replace(/\.(?:js|ts)$/, MANUAL_TEST_SUFFIX);
|
|
117
117
|
return htmlSpecifier == scriptSpecifier ? void 0 : getManualPages().get(htmlSpecifier);
|
|
118
118
|
};
|
|
119
119
|
const packageThemeEntryCache = /* @__PURE__ */ new Map();
|
|
120
|
-
const
|
|
120
|
+
const getPackageThemeEntries = (packageRootSpecifier) => {
|
|
121
121
|
if (!packageThemeEntryCache.has(packageRootSpecifier)) {
|
|
122
|
-
const
|
|
123
|
-
packageThemeEntryCache.set(packageRootSpecifier,
|
|
122
|
+
const themeEntries = THEME_ENTRY_FILE_PATHS.filter((themeEntryFilePath) => existsSync(resolve(workspaceRoot, stripLeadingSlash(packageRootSpecifier), themeEntryFilePath)));
|
|
123
|
+
packageThemeEntryCache.set(packageRootSpecifier, themeEntries);
|
|
124
124
|
}
|
|
125
125
|
return packageThemeEntryCache.get(packageRootSpecifier);
|
|
126
126
|
};
|
|
@@ -174,9 +174,12 @@ function manualTestsPlugin(options) {
|
|
|
174
174
|
const entry = getManualPageEntryForScriptSpecifier(scriptSpecifier);
|
|
175
175
|
if (!entry) return;
|
|
176
176
|
const packageRootSpecifier = entry.htmlFilePath.slice(0, entry.htmlFilePath.indexOf(MANUAL_TESTS_DIRECTORY));
|
|
177
|
-
|
|
177
|
+
const themeEntries = getPackageThemeEntries(packageRootSpecifier);
|
|
178
|
+
if (!themeEntries.length) return;
|
|
178
179
|
return {
|
|
179
|
-
code: `${code}\
|
|
180
|
+
code: `${code}\n${themeEntries.map((themeEntryFilePath) => {
|
|
181
|
+
return `import '${posix.relative(posix.dirname(scriptSpecifier), `${packageRootSpecifier}/${themeEntryFilePath}`)}';`;
|
|
182
|
+
}).join("\n")}\n`,
|
|
180
183
|
map: null
|
|
181
184
|
};
|
|
182
185
|
}
|
|
@@ -324,126 +327,6 @@ function parseRequestUrl(requestUrl) {
|
|
|
324
327
|
return URL.parse(requestUrl || "", "http://localhost");
|
|
325
328
|
}
|
|
326
329
|
//#endregion
|
|
327
|
-
//#region src/static-assets-plugin/static-assets.ts
|
|
328
|
-
/**
|
|
329
|
-
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
330
|
-
* For licensing, see LICENSE.md.
|
|
331
|
-
*/
|
|
332
|
-
const PROCESSED_MANUAL_TEST_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".ts"]);
|
|
333
|
-
const MANUAL_TEST_SUFFIX = ".manual.html";
|
|
334
|
-
const VITE_MODULE_QUERY_PARAMETERS = /* @__PURE__ */ new Set([
|
|
335
|
-
"import",
|
|
336
|
-
"raw",
|
|
337
|
-
"url",
|
|
338
|
-
"worker",
|
|
339
|
-
"inline"
|
|
340
|
-
]);
|
|
341
|
-
function collectManualStaticAssets(patterns, workspaceRoot) {
|
|
342
|
-
return new Map(patterns.map((pattern) => `${pattern.replace(/\/+$/, "")}/tests/manual/**/*`).flatMap((pattern) => globSync(pattern, { cwd: workspaceRoot })).sort().filter((relativeFilePath) => statSync(resolve(workspaceRoot, relativeFilePath), { throwIfNoEntry: false })?.isFile()).filter(isManualStaticAssetPath).map((relativeFilePath) => [toPublicSpecifier(relativeFilePath), resolve(workspaceRoot, relativeFilePath)]));
|
|
343
|
-
}
|
|
344
|
-
function createManualStaticAssetsMiddleware(collectStaticAssets) {
|
|
345
|
-
return (request, response, next) => {
|
|
346
|
-
if (request.method != "GET" && request.method != "HEAD") {
|
|
347
|
-
next();
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
const publicPath = getManualStaticAssetPublicPath(request.url);
|
|
351
|
-
const filePath = publicPath && collectStaticAssets().get(publicPath);
|
|
352
|
-
const fileStats = filePath ? statSync(filePath, { throwIfNoEntry: false }) : void 0;
|
|
353
|
-
if (!filePath || !fileStats?.isFile()) {
|
|
354
|
-
next();
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
response.statusCode = 200;
|
|
358
|
-
response.setHeader("Content-Length", fileStats.size);
|
|
359
|
-
response.setHeader("Content-Type", getContentType(extname(filePath)));
|
|
360
|
-
if (request.method == "HEAD") {
|
|
361
|
-
response.end();
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
const fileStream = createReadStream(filePath);
|
|
365
|
-
fileStream.on("error", () => response.destroy());
|
|
366
|
-
fileStream.pipe(response);
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
function getManualStaticAssetPublicPath(requestUrl) {
|
|
370
|
-
const url = URL.parse(requestUrl || "", "http://localhost");
|
|
371
|
-
if (!url) return null;
|
|
372
|
-
if ([...url.searchParams.keys()].some((key) => VITE_MODULE_QUERY_PARAMETERS.has(key))) return null;
|
|
373
|
-
const pathname = decodePathname(url.pathname);
|
|
374
|
-
if (!pathname || !isManualStaticAssetPath(pathname)) return null;
|
|
375
|
-
return pathname;
|
|
376
|
-
}
|
|
377
|
-
function decodePathname(pathname) {
|
|
378
|
-
try {
|
|
379
|
-
return decodeURIComponent(pathname);
|
|
380
|
-
} catch {
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
function isManualStaticAssetPath(filePath) {
|
|
385
|
-
if (filePath.endsWith(MANUAL_TEST_SUFFIX)) return false;
|
|
386
|
-
return extname(filePath) != "" && !PROCESSED_MANUAL_TEST_EXTENSIONS.has(extname(filePath));
|
|
387
|
-
}
|
|
388
|
-
function getContentType(extension) {
|
|
389
|
-
switch (extension) {
|
|
390
|
-
case ".avif": return "image/avif";
|
|
391
|
-
case ".css": return "text/css; charset=utf-8";
|
|
392
|
-
case ".gif": return "image/gif";
|
|
393
|
-
case ".htm":
|
|
394
|
-
case ".html": return "text/html; charset=utf-8";
|
|
395
|
-
case ".ico": return "image/x-icon";
|
|
396
|
-
case ".jpg":
|
|
397
|
-
case ".jpeg": return "image/jpeg";
|
|
398
|
-
case ".json":
|
|
399
|
-
case ".map": return "application/json; charset=utf-8";
|
|
400
|
-
case ".mp3": return "audio/mpeg";
|
|
401
|
-
case ".mp4": return "video/mp4";
|
|
402
|
-
case ".png": return "image/png";
|
|
403
|
-
case ".svg": return "image/svg+xml";
|
|
404
|
-
case ".txt": return "text/plain; charset=utf-8";
|
|
405
|
-
case ".webp": return "image/webp";
|
|
406
|
-
case ".woff": return "font/woff";
|
|
407
|
-
case ".woff2": return "font/woff2";
|
|
408
|
-
default: return "application/octet-stream";
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
//#endregion
|
|
412
|
-
//#region src/static-assets-plugin/plugin.ts
|
|
413
|
-
/**
|
|
414
|
-
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
415
|
-
* For licensing, see LICENSE.md.
|
|
416
|
-
*/
|
|
417
|
-
function manualStaticAssetsPlugin(options) {
|
|
418
|
-
let workspaceRoot = process.cwd();
|
|
419
|
-
const includePackageNames = (options.include || []).filter(Boolean);
|
|
420
|
-
const getManualStaticAssets = cacheValue(() => filterManualStaticAssets(collectManualStaticAssets(options.paths, workspaceRoot), includePackageNames, workspaceRoot)).get;
|
|
421
|
-
return {
|
|
422
|
-
name: "ckeditor5-manual-static-assets",
|
|
423
|
-
configResolved(config) {
|
|
424
|
-
workspaceRoot = config.root;
|
|
425
|
-
},
|
|
426
|
-
configureServer(server) {
|
|
427
|
-
server.middlewares.use(createManualStaticAssetsMiddleware(getManualStaticAssets));
|
|
428
|
-
},
|
|
429
|
-
generateBundle() {
|
|
430
|
-
for (const [publicPath, filePath] of getManualStaticAssets()) this.emitFile({
|
|
431
|
-
type: "asset",
|
|
432
|
-
fileName: stripLeadingSlash(publicPath),
|
|
433
|
-
source: readFileSync(filePath)
|
|
434
|
-
});
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
function filterManualStaticAssets(staticAssets, includePackageNames, workspaceRoot) {
|
|
439
|
-
const isIncluded = createPackageNameFilter(includePackageNames);
|
|
440
|
-
return new Map([...staticAssets].filter(([publicPath]) => isIncluded(getManualTestPackageName(publicPath, workspaceRoot))));
|
|
441
|
-
}
|
|
442
|
-
function getManualTestPackageName(publicPath, workspaceRoot) {
|
|
443
|
-
if (publicPath.startsWith("/tests/manual/")) return basename(workspaceRoot);
|
|
444
|
-
return publicPath.match(/^\/(?:.*\/)?([^/]+)\/tests\/manual\//)?.[1] || null;
|
|
445
|
-
}
|
|
446
|
-
//#endregion
|
|
447
330
|
//#region src/debug-plugin/plugin.ts
|
|
448
331
|
/**
|
|
449
332
|
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
@@ -598,4 +481,4 @@ function rawSvgPlugin() {
|
|
|
598
481
|
* For licensing, see LICENSE.md.
|
|
599
482
|
*/
|
|
600
483
|
//#endregion
|
|
601
|
-
export { ckDebugPlugin,
|
|
484
|
+
export { ckDebugPlugin, manualTestsPlugin, rawSvgPlugin, refreshPlugin, stringifyValues };
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import type { ManualPageEntry } from "./types.js";
|
|
6
6
|
/**
|
|
7
7
|
* Discovers manual test pages. A manual test is a single full HTML document named
|
|
8
|
-
* `<slug>.manual.html` located under a package's `
|
|
8
|
+
* `<slug>.manual.html` located under a package's `manual/` directory. Plain `.html`
|
|
9
9
|
* files are treated as static fixtures and are never collected here.
|
|
10
10
|
*
|
|
11
11
|
* The `patterns` are package root globs, for example `packages/*`.
|
package/package.json
CHANGED
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
interface FavoritableEntry {
|
|
7
|
+
packageName: string;
|
|
8
|
+
slug: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const FAVORITES_STORAGE_KEY = 'ckeditor5-manual-test-favorites';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns the stable identifier used to persist a manual test as a favorite.
|
|
15
|
+
*/
|
|
16
|
+
export function getFavoriteId( entry: FavoritableEntry ): string {
|
|
17
|
+
return `${ entry.packageName }/${ entry.slug }`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Returns the accessible label for a favorite toggle action.
|
|
22
|
+
*/
|
|
23
|
+
export function getFavoriteActionLabel( isFavorite: boolean ): string {
|
|
24
|
+
return isFavorite ? 'Remove from favorites' : 'Add to favorites';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Updates a favorite button from the current favorite identifier set.
|
|
29
|
+
*/
|
|
30
|
+
export function updateFavoriteButtonState(
|
|
31
|
+
button: HTMLButtonElement,
|
|
32
|
+
favoriteIds: Set<string>,
|
|
33
|
+
favoriteId: string
|
|
34
|
+
): void {
|
|
35
|
+
const isFavorite = favoriteIds.has( favoriteId );
|
|
36
|
+
const label = getFavoriteActionLabel( isFavorite );
|
|
37
|
+
|
|
38
|
+
button.title = label;
|
|
39
|
+
button.setAttribute( 'aria-label', label );
|
|
40
|
+
button.setAttribute( 'aria-pressed', String( isFavorite ) );
|
|
41
|
+
button.classList.toggle( 'favorite--active', isFavorite );
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Returns favorite entries in their original order.
|
|
46
|
+
*/
|
|
47
|
+
export function filterFavoriteEntries<T extends FavoritableEntry>( entries: Array<T>, favoriteIds: Set<string> ): Array<T> {
|
|
48
|
+
return entries.filter( entry => favoriteIds.has( getFavoriteId( entry ) ) );
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Toggles an identifier in the favorite set.
|
|
53
|
+
*/
|
|
54
|
+
export function toggleFavoriteId( favoriteIds: Set<string>, favoriteId: string ): void {
|
|
55
|
+
if ( favoriteIds.has( favoriteId ) ) {
|
|
56
|
+
favoriteIds.delete( favoriteId );
|
|
57
|
+
} else {
|
|
58
|
+
favoriteIds.add( favoriteId );
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Loads favorite entry identifiers from local storage.
|
|
64
|
+
*/
|
|
65
|
+
export function loadFavoriteIds(): Set<string> {
|
|
66
|
+
try {
|
|
67
|
+
const storedValue = localStorage.getItem( FAVORITES_STORAGE_KEY );
|
|
68
|
+
|
|
69
|
+
if ( !storedValue ) {
|
|
70
|
+
return new Set();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const parsedValue: unknown = JSON.parse( storedValue );
|
|
74
|
+
|
|
75
|
+
if ( !Array.isArray( parsedValue ) || parsedValue.some( value => typeof value != 'string' ) ) {
|
|
76
|
+
return new Set();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return new Set( parsedValue );
|
|
80
|
+
} catch {
|
|
81
|
+
return new Set();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Persists favorite entry identifiers in local storage.
|
|
87
|
+
*/
|
|
88
|
+
export function saveFavoriteIds( favoriteIds: Set<string> ): void {
|
|
89
|
+
try {
|
|
90
|
+
localStorage.setItem( FAVORITES_STORAGE_KEY, JSON.stringify( [ ...favoriteIds ] ) );
|
|
91
|
+
} catch {
|
|
92
|
+
// Favorites are optional, so storage failures must not block the catalog.
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
interface SearchableEntry {
|
|
7
|
+
packageName: string;
|
|
8
|
+
slug: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const PACKAGE_MATCH_PRIORITY = {
|
|
12
|
+
exactName: 0,
|
|
13
|
+
namePrefix: 1,
|
|
14
|
+
nameContains: 2,
|
|
15
|
+
testSlugOnly: 3
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
type PackageMatchPriority = typeof PACKAGE_MATCH_PRIORITY[keyof typeof PACKAGE_MATCH_PRIORITY];
|
|
19
|
+
|
|
20
|
+
interface PackageSearchResult<T extends SearchableEntry> {
|
|
21
|
+
visibleEntries: Array<T>;
|
|
22
|
+
priority: PackageMatchPriority;
|
|
23
|
+
matchingSlugCount: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MatchOffset {
|
|
27
|
+
start: number;
|
|
28
|
+
end: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Filters tests and orders matching packages by relevance:
|
|
33
|
+
*
|
|
34
|
+
* 1. Exact full or short package name, e.g. `ckeditor5-table` or `table`.
|
|
35
|
+
* 2. Package name whose full or short name starts with the query.
|
|
36
|
+
* 3. Package name containing the query.
|
|
37
|
+
* 4. Packages with matching test slugs only.
|
|
38
|
+
*
|
|
39
|
+
* Packages at the same priority are ordered by the number of matching test slugs. Stable sorting
|
|
40
|
+
* preserves the original package order when that number is also equal. Tests within a package
|
|
41
|
+
* always retain their original order.
|
|
42
|
+
*
|
|
43
|
+
* A package-name match shows all tests from that package. A slug match shows only matching tests.
|
|
44
|
+
*/
|
|
45
|
+
export function filterEntries<T extends SearchableEntry>( entries: Array<T>, normalizedQuery: string ): Array<T> {
|
|
46
|
+
if ( !normalizedQuery ) {
|
|
47
|
+
return entries;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const packageGroups = groupEntriesByPackage( entries );
|
|
51
|
+
const searchResults = [ ...packageGroups.entries() ]
|
|
52
|
+
.map( ( [ packageName, packageEntries ] ) => createPackageSearchResult(
|
|
53
|
+
packageName,
|
|
54
|
+
packageEntries,
|
|
55
|
+
normalizedQuery
|
|
56
|
+
) )
|
|
57
|
+
.filter( result => result.visibleEntries.length );
|
|
58
|
+
|
|
59
|
+
searchResults.sort( comparePackageSearchResults );
|
|
60
|
+
|
|
61
|
+
return searchResults.flatMap( result => result.visibleEntries );
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Groups entries by package while preserving their original order.
|
|
66
|
+
*/
|
|
67
|
+
function groupEntriesByPackage<T extends SearchableEntry>( entries: Array<T> ): Map<string, Array<T>> {
|
|
68
|
+
const packageGroups = new Map<string, Array<T>>();
|
|
69
|
+
|
|
70
|
+
for ( const entry of entries ) {
|
|
71
|
+
const packageEntries = packageGroups.get( entry.packageName ) || [];
|
|
72
|
+
|
|
73
|
+
packageEntries.push( entry );
|
|
74
|
+
packageGroups.set( entry.packageName, packageEntries );
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return packageGroups;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Creates the visible entries and ranking metadata for one package.
|
|
82
|
+
*/
|
|
83
|
+
function createPackageSearchResult<T extends SearchableEntry>(
|
|
84
|
+
packageName: string,
|
|
85
|
+
packageEntries: Array<T>,
|
|
86
|
+
normalizedQuery: string
|
|
87
|
+
): PackageSearchResult<T> {
|
|
88
|
+
const priority = getPackageMatchPriority( packageName, normalizedQuery );
|
|
89
|
+
const matchingSlugEntries = packageEntries.filter( entry => entry.slug.toLowerCase().includes( normalizedQuery ) );
|
|
90
|
+
const packageNameMatches = priority != PACKAGE_MATCH_PRIORITY.testSlugOnly;
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
visibleEntries: packageNameMatches ? packageEntries : matchingSlugEntries,
|
|
94
|
+
priority,
|
|
95
|
+
matchingSlugCount: matchingSlugEntries.length
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Orders package search results by match priority and matching test count.
|
|
101
|
+
*/
|
|
102
|
+
function comparePackageSearchResults<T extends SearchableEntry>(
|
|
103
|
+
first: PackageSearchResult<T>,
|
|
104
|
+
second: PackageSearchResult<T>
|
|
105
|
+
): number {
|
|
106
|
+
return first.priority - second.priority ||
|
|
107
|
+
second.matchingSlugCount - first.matchingSlugCount;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Returns how closely a package name matches the normalized query.
|
|
112
|
+
*/
|
|
113
|
+
function getPackageMatchPriority( packageName: string, normalizedQuery: string ): PackageMatchPriority {
|
|
114
|
+
const normalizedPackageName = packageName.toLowerCase();
|
|
115
|
+
const searchablePackageNames = [ normalizedPackageName, normalizedPackageName.replace( /^ckeditor5-/, '' ) ];
|
|
116
|
+
|
|
117
|
+
if ( searchablePackageNames.includes( normalizedQuery ) ) {
|
|
118
|
+
return PACKAGE_MATCH_PRIORITY.exactName;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if ( searchablePackageNames.some( name => name.startsWith( normalizedQuery ) ) ) {
|
|
122
|
+
return PACKAGE_MATCH_PRIORITY.namePrefix;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if ( searchablePackageNames.some( name => name.includes( normalizedQuery ) ) ) {
|
|
126
|
+
return PACKAGE_MATCH_PRIORITY.nameContains;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return PACKAGE_MATCH_PRIORITY.testSlugOnly;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Returns offsets of all non-overlapping, case-insensitive query matches in text.
|
|
134
|
+
*/
|
|
135
|
+
export function findMatchOffsets( text: string, normalizedQuery: string ): Array<MatchOffset> {
|
|
136
|
+
if ( !normalizedQuery ) {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const normalizedText = text.toLowerCase();
|
|
141
|
+
const matches: Array<MatchOffset> = [];
|
|
142
|
+
let matchStart = normalizedText.indexOf( normalizedQuery );
|
|
143
|
+
|
|
144
|
+
while ( matchStart != -1 ) {
|
|
145
|
+
const matchEnd = matchStart + normalizedQuery.length;
|
|
146
|
+
|
|
147
|
+
matches.push( { start: matchStart, end: matchEnd } );
|
|
148
|
+
matchStart = normalizedText.indexOf( normalizedQuery, matchEnd );
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return matches;
|
|
152
|
+
}
|
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-
|
|
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
|
-
|
|
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-clear {
|
|
147
|
+
margin-inline-end: 10px;
|
|
148
|
+
padding: 6px;
|
|
149
|
+
line-height: 0;
|
|
150
|
+
background: none;
|
|
151
|
+
border: 0;
|
|
152
|
+
border-radius: var(--radius-sm);
|
|
153
|
+
cursor: pointer;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.app__search-clear:hover {
|
|
157
|
+
color: var(--text);
|
|
158
|
+
background: var(--surface-hover);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.app__search:has(.app__search-input:placeholder-shown) .app__search-clear {
|
|
162
|
+
visibility: hidden;
|
|
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
|
-
|
|
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
|
|
181
|
-
|
|
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;
|
package/theme/catalog.html
CHANGED
|
@@ -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
|
-
<
|
|
19
|
-
<
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
166
|
+
* Highlights query matches in the currently rendered package and test names.
|
|
90
167
|
*/
|
|
91
|
-
function
|
|
92
|
-
|
|
168
|
+
function updateSearchHighlights( root: HTMLElement, normalizedQuery: string ): void {
|
|
169
|
+
if ( !( 'highlights' in CSS ) ) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
93
172
|
|
|
94
|
-
|
|
95
|
-
|
|
173
|
+
CSS.highlights.delete( SEARCH_HIGHLIGHT_NAME );
|
|
174
|
+
|
|
175
|
+
if ( !normalizedQuery ) {
|
|
176
|
+
return;
|
|
96
177
|
}
|
|
97
178
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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 ] ) =>
|
|
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
|
-
|
|
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:
|
|
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% -
|
|
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%,
|
|
30
|
+
transform: translate(-50%, 0);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
.refresh-prompt:hover {
|
package/theme/manual-header.css
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
139
|
+
top: 0;
|
|
123
140
|
right: 0;
|
|
124
141
|
bottom: 0;
|
|
125
142
|
z-index: 99998;
|
package/theme/manual-header.ts
CHANGED
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
// Imported as raw strings so they can be adopted into constructable stylesheets.
|
|
8
8
|
import headerStyles from './manual-header.css?inline';
|
|
9
9
|
import instructionsStyles from './manual-instructions.css?inline';
|
|
10
|
+
import {
|
|
11
|
+
getFavoriteId,
|
|
12
|
+
loadFavoriteIds,
|
|
13
|
+
saveFavoriteIds,
|
|
14
|
+
toggleFavoriteId,
|
|
15
|
+
updateFavoriteButtonState
|
|
16
|
+
} from './catalog-favorites.js';
|
|
17
|
+
import { createFavoriteIcon } from './favorite-icon.js';
|
|
10
18
|
|
|
11
19
|
const MANUAL_HEADER_DATA_META = 'ck-manual-header';
|
|
12
20
|
|
|
@@ -19,6 +27,7 @@ const TEMPLATE = /* html */ `
|
|
|
19
27
|
<a class="pill back" title="Back to test index">← All tests</a>
|
|
20
28
|
<span class="divider" aria-hidden="true"></span>
|
|
21
29
|
<span class="label">
|
|
30
|
+
<button type="button" class="pill favorite" data-action="favorite"></button>
|
|
22
31
|
<span class="label__name" data-field="name"></span>
|
|
23
32
|
<span class="label__package" data-field="package"></span>
|
|
24
33
|
</span>
|
|
@@ -47,9 +56,9 @@ const TEMPLATE = /* html */ `
|
|
|
47
56
|
/* eslint-enable @stylistic/max-len */
|
|
48
57
|
|
|
49
58
|
/**
|
|
50
|
-
* `<ck-manual-header>` renders the manual test chrome: a
|
|
51
|
-
*
|
|
52
|
-
* projects the element's light-DOM children (default slot).
|
|
59
|
+
* `<ck-manual-header>` renders the manual test chrome: a header bar with the test name,
|
|
60
|
+
* package name, back-to-index link and favorite toggle, plus a collapsible instructions
|
|
61
|
+
* panel that projects the element's light-DOM children (default slot).
|
|
53
62
|
*
|
|
54
63
|
* The manual test server injects a `<meta name="ck-manual-header">` carrying the package
|
|
55
64
|
* name and the base-aware catalog href, and the `<script>` that defines this
|
|
@@ -61,6 +70,7 @@ const TEMPLATE = /* html */ `
|
|
|
61
70
|
*/
|
|
62
71
|
class ManualHeaderElement extends HTMLElement {
|
|
63
72
|
private _panel!: HTMLElement;
|
|
73
|
+
private _favoriteId!: string;
|
|
64
74
|
|
|
65
75
|
constructor() {
|
|
66
76
|
super();
|
|
@@ -69,6 +79,7 @@ class ManualHeaderElement extends HTMLElement {
|
|
|
69
79
|
|
|
70
80
|
shadow.adoptedStyleSheets = [ headerStyleSheet ];
|
|
71
81
|
shadow.innerHTML = TEMPLATE;
|
|
82
|
+
shadow.querySelector<HTMLElement>( '[data-action="favorite"]' )!.append( createFavoriteIcon( 'favorite__icon' ) );
|
|
72
83
|
}
|
|
73
84
|
|
|
74
85
|
public connectedCallback(): void {
|
|
@@ -82,6 +93,10 @@ class ManualHeaderElement extends HTMLElement {
|
|
|
82
93
|
const backLink = shadow.querySelector<HTMLAnchorElement>( '.back' )!;
|
|
83
94
|
|
|
84
95
|
backLink.href = data.catalogHref;
|
|
96
|
+
this._favoriteId = getFavoriteId( {
|
|
97
|
+
packageName: data.packageName,
|
|
98
|
+
slug: getSlugFromLocation()
|
|
99
|
+
} );
|
|
85
100
|
|
|
86
101
|
for ( const element of shadow.querySelectorAll<HTMLElement>( '[data-field="name"]' ) ) {
|
|
87
102
|
element.textContent = this._resolveDisplayName();
|
|
@@ -91,6 +106,7 @@ class ManualHeaderElement extends HTMLElement {
|
|
|
91
106
|
element.textContent = data.packageName;
|
|
92
107
|
}
|
|
93
108
|
|
|
109
|
+
this._renderFavoriteAction( loadFavoriteIds() );
|
|
94
110
|
this._setUpInstructions();
|
|
95
111
|
this._setUpActions( shadow );
|
|
96
112
|
}
|
|
@@ -129,7 +145,9 @@ class ManualHeaderElement extends HTMLElement {
|
|
|
129
145
|
return;
|
|
130
146
|
}
|
|
131
147
|
|
|
132
|
-
if ( trigger.dataset.action == '
|
|
148
|
+
if ( trigger.dataset.action == 'favorite' ) {
|
|
149
|
+
this._toggleFavorite();
|
|
150
|
+
} else if ( trigger.dataset.action == 'toggle' ) {
|
|
133
151
|
this._toggleInstructions( !this._panel.classList.contains( 'panel--open' ) );
|
|
134
152
|
} else if ( trigger.dataset.action == 'close' ) {
|
|
135
153
|
this._toggleInstructions( false );
|
|
@@ -143,6 +161,20 @@ class ManualHeaderElement extends HTMLElement {
|
|
|
143
161
|
} );
|
|
144
162
|
}
|
|
145
163
|
|
|
164
|
+
private _toggleFavorite(): void {
|
|
165
|
+
const favoriteIds = loadFavoriteIds();
|
|
166
|
+
|
|
167
|
+
toggleFavoriteId( favoriteIds, this._favoriteId );
|
|
168
|
+
saveFavoriteIds( favoriteIds );
|
|
169
|
+
this._renderFavoriteAction( favoriteIds );
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private _renderFavoriteAction( favoriteIds: Set<string> ): void {
|
|
173
|
+
const trigger = this.shadowRoot!.querySelector<HTMLButtonElement>( '[data-action="favorite"]' )!;
|
|
174
|
+
|
|
175
|
+
updateFavoriteButtonState( trigger, favoriteIds, this._favoriteId );
|
|
176
|
+
}
|
|
177
|
+
|
|
146
178
|
private _toggleInstructions( isOpen: boolean ): void {
|
|
147
179
|
const trigger = this.shadowRoot!.querySelector<HTMLElement>( '[data-action="toggle"]' )!;
|
|
148
180
|
|
|
@@ -11,12 +11,20 @@
|
|
|
11
11
|
* reach nested nodes, so instruction typography is scoped here to `ck-manual-header` descendants.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
/*
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
/*
|
|
15
|
+
* `:where()` keeps zero specificity so anything a test styles itself takes precedence.
|
|
16
|
+
*/
|
|
17
|
+
:where(*, *::before, *::after) {
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
:where(body) {
|
|
22
|
+
margin: 0;
|
|
23
|
+
min-height: 100vh;
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
ck-manual-header {
|
|
27
|
+
margin-bottom: 12px;
|
|
20
28
|
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
21
29
|
line-height: 1.6;
|
|
22
30
|
color: #0a0a0a;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
-
* For licensing, see LICENSE.md.
|
|
4
|
-
*/
|
|
5
|
-
import type { Plugin } from "vite";
|
|
6
|
-
export interface ManualStaticAssetsPluginOptions {
|
|
7
|
-
paths: Array<string>;
|
|
8
|
-
include?: Array<string>;
|
|
9
|
-
}
|
|
10
|
-
export declare function manualStaticAssetsPlugin(options: ManualStaticAssetsPluginOptions): Plugin;
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license Copyright (c) 2003-2026, CKSource Holding sp. z o.o. All rights reserved.
|
|
3
|
-
* For licensing, see LICENSE.md.
|
|
4
|
-
*/
|
|
5
|
-
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
6
|
-
type ManualStaticAssetsMiddleware = (request: IncomingMessage, response: ServerResponse, next: () => void) => void;
|
|
7
|
-
export declare function collectManualStaticAssets(patterns: Array<string>, workspaceRoot: string): Map<string, string>;
|
|
8
|
-
export declare function createManualStaticAssetsMiddleware(collectStaticAssets: () => Map<string, string>): ManualStaticAssetsMiddleware;
|
|
9
|
-
export {};
|