@keenmate/web-components-devkit 1.0.0-rc10 → 1.0.0-rc11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +25 -8
- package/package.json +1 -1
- package/src/cli/commands/doctor.js +108 -26
- package/src/config.d.ts +14 -0
- package/src/config.js +20 -1
- package/src/examples/constants.js +6 -0
- package/src/examples/css/page-footer.css +19 -0
- package/src/examples/css/style.css +1 -0
- package/src/examples/index-page.js +2 -11
- package/src/examples/index.d.ts +6 -1
- package/src/examples/index.js +1 -0
- package/src/examples/kit.js +5 -3
- package/src/examples/page-footer.js +44 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.0-rc11] - 2026-11-06 [PUBLISHED]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Doctor understands monorepos: `kmwc.packages`.** Doctor assumed the repo root was the package.
|
|
8
|
+
In web-dropzone (npm workspaces: a private root holding the example site,
|
|
9
|
+
`packages/web-dropzone` the component, `packages/web-dropzone-core` a headless library) that gave
|
|
10
|
+
113 errors that said little: `npm pack` on the private root "leaked" 101 files npm never publishes,
|
|
11
|
+
six rulebook suites couldn't find a component at the root, the overlay suite scanned
|
|
12
|
+
`node_modules`, and the loader check silently skipped — while the component's real findings
|
|
13
|
+
(129 non-BEM classes, no Custom Elements Manifest setup …) never showed. `"packages": [{ "path",
|
|
14
|
+
"kind": "component" | "library" }]` lists what the repo publishes: doctor packs each package, runs
|
|
15
|
+
the rulebook suites for its kind inside it (`rulebook:<suite>@<path>`; publish-command and
|
|
16
|
+
example-pages stay repo-wide at the root), and checks the loader class per component package.
|
|
17
|
+
Doctor warns when npm workspaces aren't all listed, errors on a listed path without a
|
|
18
|
+
package.json, and never packs a private workspace root. Without `packages` a repo is one package
|
|
19
|
+
at its root, as before.
|
|
20
|
+
- **Every example page gets the footer.** Only the index had one ("Created by Keenmate s.r.o. |
|
|
21
|
+
GitHub | web-components-devkit vX"); the other pages just ended. The kit now appends the same
|
|
22
|
+
line to every page but the index (`mountPageFooter()`, `.kmwc-page-footer`), inside the layout's
|
|
23
|
+
container. The index and the page footer render it from one function (`renderFooterLineHtml()`),
|
|
24
|
+
so they can't drift.
|
|
25
|
+
|
|
3
26
|
## [1.0.0-rc10] - 2026-11-05 [PUBLISHED]
|
|
4
27
|
|
|
5
28
|
### Added
|
package/README.md
CHANGED
|
@@ -19,6 +19,11 @@ them in the same shape: `@keenmate/web-components-devkit`.
|
|
|
19
19
|
> positioning) lives in [`@keenmate/web-components-core`](../web-components-core).
|
|
20
20
|
> Nothing here ships inside a component's bundle.
|
|
21
21
|
|
|
22
|
+
## What's New in v1.0.0-rc11
|
|
23
|
+
|
|
24
|
+
- **Doctor — monorepos: `kmwc.packages` lists what the repo publishes** — Doctor used to assume the repo root was the package. In web-dropzone (npm workspaces: a private root that holds the example site, the component in `packages/web-dropzone`, a headless library in `packages/web-dropzone-core`) that produced 113 errors that said little. `npm pack` on the private root "leaked" 101 files npm never publishes, six rulebook suites found no component at the root, and the overlay suite scanned `node_modules`. Meanwhile the component's real findings never showed. Now `"packages": [{ "path", "kind": "component" | "library" }]` in `package.json#kmwc` lists the published packages. Doctor packs each one and runs the rulebook suites for its kind inside it (reported as `rulebook:<suite>@<path>`). The publish-command and example-pages suites stay repo-wide, and the loader check runs per component package. Doctor warns when npm workspaces aren't all listed, errors on a listed path without a `package.json`, and never packs a private workspace root. A repo without `packages` is still one package at its root.
|
|
25
|
+
- **Examples kit — every example page ends with the footer** — Only the index page had the "Created by Keenmate s.r.o. | GitHub | web-components-devkit vX" footer; the other example pages just stopped. The kit now appends the same line to every page except the index (`mountPageFooter()`, `.kmwc-page-footer`), at the end of the layout's container. The index footer and the page footer are rendered by one function, `renderFooterLineHtml()`, so they can't drift apart. The index keeps its own extra links (README, API docs); the page footer carries Keenmate, the repo's GitHub link and the devkit version.
|
|
26
|
+
|
|
22
27
|
## What's New in v1.0.0-rc10
|
|
23
28
|
|
|
24
29
|
- **Doctor — runs the variable-manifest check** — `kmwc check-vars` only ran inside `npm run build`, so a red manifest stayed invisible until someone built. During the devkit pass, web-treeview's `check:vars` failed on core's `--base-accent-color` (a name outside the base-variables contract) while `kmwc doctor` reported nothing. Doctor now reports the same verdict under `vars`: `--base-*` reads missing from the manifest, manifest entries the CSS no longer reads, and dead `--<prefix>-*` entries are errors; undocumented `--<prefix>-*` variables are a warning. Both commands share one function, `loadManifestVerdict()`, so they can't disagree.
|
|
@@ -28,11 +33,6 @@ them in the same shape: `@keenmate/web-components-devkit`.
|
|
|
28
33
|
- **Examples kit — inline code readable in light mode** — Inline `code` used the raw accent colour on its pale chip, which read at about 3.3:1 for the default accent. The text keeps the accent's hue at no more than 0.5 lightness. The e2e spec checks five accents in both themes for at least 4.5:1.
|
|
29
34
|
- **Devkit tests — the critical paths** — The suite now runs a real library build of the playground and checks that the emitted files, the UMD global and every export `index.d.ts` declares are exactly what package.json and doctor expect. It also checks that `kmwc new` scaffolds a repo doctor finds clean, that `kmwc sync` never rewrites a local file or an existing script, and that `check-release` keeps an rc off `latest`. Doctor itself is tested end to end on fixture repos.
|
|
30
35
|
|
|
31
|
-
## What's New in v1.0.0-rc09
|
|
32
|
-
|
|
33
|
-
- **check-pages — page CSS that can't reach into a shadow root** — web-treeview's custom-layout and responsive pages styled the rows their `renderNodeCallback` draws with page CSS (`.file-icon`, `.node-badge`, `.isco-row`, `.dev-badge` …, 18 rules). Those rows live in the tree's shadow root, and page CSS stops at the shadow boundary, so none of the rules ever applied. Nothing in the source shows where a render callback's output ends up, so this needs the loaded page. `kmwc check-pages` now reads the page's own rules (its `<style>` blocks and `examples-local.css`) and finds what each one styles: the last part of the selector, without pseudo-classes. It reports a rule when that element exists nowhere in the document but does exist inside a component's shadow root, and names the component. Run against treeview's pages from before its fix, it lists exactly those 18 rules. The fix is a CSS variable, `::part`, or the component's styles hook (`getCustomStylesCallback`).
|
|
34
|
-
- **Examples kit — the grey secondary button in dark mode** — `button.secondary` / `.btn-secondary` had white text on `#9ca3af` in dark mode, at 2.54:1. That grey sits just under the 0.34 luminance switch in the formula that picks a button's text colour, so the formula chose white. It showed on 9 web-treeview pages ("Clear", "Reset", "Move up" …) and on every other component site's grey buttons. The button now has its own fill, `--kmwc-neutral-fill-color` (`#6b7280` light / `#4b5563` dark), plus a hover token, so white text stays at 4.5:1 or better in both themes, hovered too. Muted status dots and pills keep the lighter `--kmwc-neutral-color`. An e2e spec checks the contrast in both themes.
|
|
35
|
-
|
|
36
36
|
## Catalogue
|
|
37
37
|
|
|
38
38
|
<!-- kmwc:catalogue:start — generated by `npm run readme`, do not edit inside -->
|
|
@@ -44,7 +44,7 @@ them in the same shape: `@keenmate/web-components-devkit`.
|
|
|
44
44
|
| **web-daterangepicker** | `@keenmate/web-daterangepicker` | `drp` | 12200 | [repo](https://github.com/keenmate/web-daterangepicker) · [docs](https://web-daterangepicker.keenmate.dev) · `web-daterangepicker-showcase` | ✅ | Lightweight date picker web component with excellent keyboard navigation and range selection |
|
|
45
45
|
| **web-treeview** | `@keenmate/web-treeview` | `wtv` | 12300 | [repo](https://github.com/keenmate/web-treeview) · `web-treeview-showcase` | ✅ | Lightweight treeview web component with hierarchical data display, search, expand/collapse, drag-drop, and… |
|
|
46
46
|
| **web-commandpalette** | `@keenmate/web-commandpalette` | `cp` | 12400 | [repo](https://github.com/keenmate/web-commandpalette) · [examples](https://examples.web-commandpalette.keenmate.dev) | ✅ | Framework-agnostic command-palette web component — modes, multi-step commands, scoped + global search,… |
|
|
47
|
-
| **web-dropzone** | `@keenmate/web-dropzone` | `dz` | 12500 | [repo](https://github.com/keenmate/web-dropzone) |
|
|
47
|
+
| **web-dropzone** | `@keenmate/web-dropzone` | `dz` | 12500 | [repo](https://github.com/keenmate/web-dropzone) | ✅ | Lightweight file dropzone web component with drag-drop, previews, validation, and multiple display modes. |
|
|
48
48
|
| **web-grid** | `@keenmate/web-grid` | `wg` | 12600 | [repo](https://github.com/keenmate/web-grid) · [docs](https://web-grid.keenmate.com) · [showcase](https://web-grid.keenmate.com) | — | Framework-agnostic data grid web component with sorting, filtering, inline editing, and keyboard navigation |
|
|
49
49
|
| **web-editor** | `@keenmate/web-editor` | `km` | 12700 | [examples](https://examples.web-editor.keenmate.dev) | ✅ | Keenmate Web Editor — a framework-agnostic WYSIWYG / Markdown editor built on ProseMirror. |
|
|
50
50
|
| **web-switch** | `@keenmate/web-switch` | — | 12800 | [repo](https://github.com/keenmate/web-switch) | — | Framework-agnostic web component wrapper for @keenmate/svelte-switch. Works with React, Vue, Angular, or… |
|
|
@@ -106,7 +106,7 @@ in `package.json#kmwc`, and gets the rest.
|
|
|
106
106
|
| Piece | Import / command | Replaces |
|
|
107
107
|
|---|---|---|
|
|
108
108
|
| Vite preset | `@keenmate/web-components-devkit/vite` → `defineComponentConfig()` | per-repo `vite.config.ts`, `vite.examples.config.ts` |
|
|
109
|
-
| Examples kit (auto-injected) | shared styles, chapter nav, copy-link buttons, theme switch, version badge, analytics | `examples-shared.css`, `examples-chapter-nav.js`, `examples-copy-link.js`, `plausible-snippet.html`, Dockerfile `sed` |
|
|
109
|
+
| Examples kit (auto-injected) | shared styles, chapter nav, copy-link buttons, theme switch, version badge, page footer, analytics | `examples-shared.css`, `examples-chapter-nav.js`, `examples-copy-link.js`, `plausible-snippet.html`, Dockerfile `sed` |
|
|
110
110
|
| Fragment pages | `<meta name="kmwc:title">` pages + `<kmwc-include>` partials + `<kmwc-demo>` (live markup = snippet) | the head / header / container boilerplate copied into every page, hand-copied `.code-block` snippets |
|
|
111
111
|
| Examples helpers | `@keenmate/web-components-devkit/examples` → `attachControlsPersistence`, `renderExamplesIndex`, `mountExamplesKit` | `examples-controls-store.js`, 150 lines of inline index-page CSS |
|
|
112
112
|
| Playwright preset | `@keenmate/web-components-devkit/playwright` → `definePlaywrightConfig()` | per-repo `playwright.config.ts` |
|
|
@@ -179,7 +179,11 @@ Every field is optional. Defaults are derived from the package.
|
|
|
179
179
|
// BlissFramework checkout above it. env KMWC_RULEBOOK_PATH wins
|
|
180
180
|
"skippedChecks": [] // e.g. ["responsive-overlay"]
|
|
181
181
|
},
|
|
182
|
-
"sync": { "skippedFiles": [] }
|
|
182
|
+
"sync": { "skippedFiles": [] }, // e.g. ["Makefile"] to opt a file out
|
|
183
|
+
"packages": [ // monorepos only (npm workspaces): what doctor checks
|
|
184
|
+
{ "path": "packages/web-dropzone", "kind": "component" },
|
|
185
|
+
{ "path": "packages/web-dropzone-core", "kind": "library" }
|
|
186
|
+
]
|
|
183
187
|
}
|
|
184
188
|
```
|
|
185
189
|
|
|
@@ -193,6 +197,16 @@ manifest check (no `check:vars` script, `kmwc check-vars` skips), for variables
|
|
|
193
197
|
the checker can't see, such as names built by string interpolation. Doctor runs
|
|
194
198
|
only the rulebook suites that apply to a library (see below).
|
|
195
199
|
|
|
200
|
+
**A monorepo** (npm workspaces, e.g. web-dropzone: a private root with the example
|
|
201
|
+
site, `packages/web-dropzone` + `packages/web-dropzone-core`) lists its published
|
|
202
|
+
packages in `kmwc.packages`, each with its kind. The root keeps the devkit's
|
|
203
|
+
example site, generated files, ports and registry checks; doctor packs each
|
|
204
|
+
package (`npm pack`), runs the rulebook suites for its kind *in* the package
|
|
205
|
+
(reported as `rulebook:<suite>@<path>`), and checks the loader class in each
|
|
206
|
+
component package. publish-command and example-pages stay repo-wide, at the root.
|
|
207
|
+
Without `packages` the root is the one package — doctor warns when npm workspaces
|
|
208
|
+
exist but aren't listed, and doesn't pack a private workspace root.
|
|
209
|
+
|
|
196
210
|
### Vite preset
|
|
197
211
|
|
|
198
212
|
```ts
|
|
@@ -336,6 +350,9 @@ doctor` lists the ones still repeating the head/header boilerplate.
|
|
|
336
350
|
shows `v1.2.3`, or `v1.2.3 (built …)` in a built site.
|
|
337
351
|
- **Landing page**: `renderExamplesIndex({ pages })` renders the header, cards,
|
|
338
352
|
footer and cross-page nav from the registry plus `kmwc` config.
|
|
353
|
+
- **Page footer**: every other example page ends with the same line as the
|
|
354
|
+
index footer — Keenmate, the repo's GitHub link, the devkit version — inside
|
|
355
|
+
the layout's container (`.kmwc-page-footer`).
|
|
339
356
|
- **Late content**: the kit mounts one task after DOM-ready. Pages that render
|
|
340
357
|
sections after a fetch call `mountExamplesKit()`.
|
|
341
358
|
- **Analytics**: the Plausible snippet goes into **built** pages only, and only
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keenmate/web-components-devkit",
|
|
3
|
-
"version": "1.0.0-
|
|
3
|
+
"version": "1.0.0-rc11",
|
|
4
4
|
"description": "Shared dev, example-site and build infrastructure for Keenmate web components: Vite preset, examples kit (chapter nav, copy-link, controls store, shared styles), Playwright preset and the `kmwc` CLI (sync, doctor, check-vars, new, readme).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Errors fail (exit 1); warnings don't.
|
|
15
15
|
*/
|
|
16
16
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
17
|
-
import { join } from 'node:path';
|
|
17
|
+
import { join, relative, resolve } from 'node:path';
|
|
18
18
|
import { spawnSync } from 'node:child_process';
|
|
19
19
|
import { getKmwcConfig, getPortsForBlock, getRegistry, LEGACY_FILES, NON_PAGE_HTML_FILES, PACKAGE_LEAK_RULES, PACKAGE_FILES_EXCLUSIONS } from '../../config.js';
|
|
20
20
|
import { getSyncPlan, getScriptsPlan, CLAUDE_BLOCK_START } from '../sync-plan.js';
|
|
@@ -66,8 +66,9 @@ export const RULEBOOK_SUITES = [
|
|
|
66
66
|
{ name: 'readme-structure', isPrefixed: false, isComponentOnly: false },
|
|
67
67
|
{ name: 'custom-elements-manifest', isPrefixed: false, isComponentOnly: true },
|
|
68
68
|
{ name: 'responsive-overlay', isPrefixed: true, isComponentOnly: true },
|
|
69
|
-
|
|
70
|
-
{ name: '
|
|
69
|
+
// Repo-wide: one /publish command and one example site per repo, checked at its root.
|
|
70
|
+
{ name: 'publish-command', isPrefixed: false, isComponentOnly: false, isRepoWide: true },
|
|
71
|
+
{ name: 'example-pages', isPrefixed: false, isComponentOnly: false, isRepoWide: true },
|
|
71
72
|
];
|
|
72
73
|
|
|
73
74
|
const LIBRARY_KIND = 'library';
|
|
@@ -122,9 +123,9 @@ export function getBashPath({ platform = process.platform, env = process.env, ch
|
|
|
122
123
|
return gitRoots.map((gitRoot) => join(gitRoot, GIT_BASH_RELATIVE_PATH)).find((bashPath) => checkExists(bashPath)) || 'bash';
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
function runRulebookSuite(config, suite) {
|
|
126
|
+
function runRulebookSuite(config, suite, checkedRoot) {
|
|
126
127
|
const scriptPath = join(config.rulebook.path, `${suite.name}.checks.sh`).replace(/\\/g, '/');
|
|
127
|
-
const scriptArgs = [scriptPath,
|
|
128
|
+
const scriptArgs = [scriptPath, checkedRoot.replace(/\\/g, '/')];
|
|
128
129
|
if (suite.isPrefixed && config.prefix) scriptArgs.push(config.prefix);
|
|
129
130
|
return spawnSync(getBashPath(), scriptArgs, { encoding: 'utf8', env: { ...process.env, NO_COLOR: '1' } });
|
|
130
131
|
}
|
|
@@ -302,8 +303,15 @@ export function checkComponent(config, doctorConfig = {}) {
|
|
|
302
303
|
if (!outputMismatches.length) addPass('library', 'package.json targets match the library build outputs');
|
|
303
304
|
}
|
|
304
305
|
|
|
306
|
+
// ── monorepo: the workspace packages doctor checks ───────────────────────
|
|
307
|
+
checkWorkspacePackages(config, { addPass, addWarning, addError });
|
|
308
|
+
|
|
305
309
|
// ── core's default loader class ──────────────────────────────────────────
|
|
306
|
-
if (config.prefix && config.kind !== LIBRARY_KIND)
|
|
310
|
+
if (config.prefix && config.kind !== LIBRARY_KIND) {
|
|
311
|
+
for (const checkedPackage of config.packages.filter((packageEntry) => packageEntry.kind === 'component')) {
|
|
312
|
+
checkLoaderClass(config, checkedPackage, { addPass, addWarning, addError });
|
|
313
|
+
}
|
|
314
|
+
}
|
|
307
315
|
|
|
308
316
|
// ── variable manifest (what `kmwc check-vars` checks; it only runs in the build) ──
|
|
309
317
|
if (config.hasPackageConfig) checkVariableManifest(config, { addPass, addWarning, addError });
|
|
@@ -320,7 +328,9 @@ export function checkComponent(config, doctorConfig = {}) {
|
|
|
320
328
|
} else if (plausibleSiteId) addPass('analytics', `Plausible site id ${plausibleSiteId} is this repo's own`);
|
|
321
329
|
|
|
322
330
|
// ── package contents ─────────────────────────────────────────────────────
|
|
323
|
-
if (doctorConfig.shouldCheckPackage !== false)
|
|
331
|
+
if (doctorConfig.shouldCheckPackage !== false) {
|
|
332
|
+
for (const checkedPackage of config.packages) checkPackageContents(checkedPackage, { addPass, addWarning, addError });
|
|
333
|
+
}
|
|
324
334
|
|
|
325
335
|
// ── rulebook suites ──────────────────────────────────────────────────────
|
|
326
336
|
if (doctorConfig.shouldRunRulebook !== false) checkRulebook(config, { addPass, addWarning, addError });
|
|
@@ -361,11 +371,15 @@ function checkPorts(config, portBlock, registry, { addPass, addWarning, addError
|
|
|
361
371
|
* before (or after) the core bump leaves the default loader unstyled, and no rulebook
|
|
362
372
|
* check sees that (a VALIDATION-NOTES `## C-NC-8` even hides a leftover old class).
|
|
363
373
|
*/
|
|
364
|
-
function checkLoaderClass(config, { addPass, addWarning, addError }) {
|
|
365
|
-
const coreLoader = loadCoreLoader(
|
|
374
|
+
function checkLoaderClass(config, checkedPackage, { addPass, addWarning, addError }) {
|
|
375
|
+
const coreLoader = loadCoreLoader(checkedPackage.root);
|
|
366
376
|
if (!coreLoader?.separator) return;
|
|
367
377
|
const coreClassName = `${config.prefix}${coreLoader.separator}loader`;
|
|
368
|
-
const
|
|
378
|
+
const styledLoaderClasses = loadStyledLoaderClasses(checkedPackage.root, config.prefix).map((styledClass) => ({
|
|
379
|
+
...styledClass,
|
|
380
|
+
file: getPackageFilePath(checkedPackage, styledClass.file),
|
|
381
|
+
}));
|
|
382
|
+
const { isCoreClassStyled, renamedClasses, collidingClasses } = getLoaderClassVerdict(coreLoader.separator, styledLoaderClasses);
|
|
369
383
|
for (const { file, className } of collidingClasses) {
|
|
370
384
|
addWarning(
|
|
371
385
|
'loader',
|
|
@@ -418,10 +432,56 @@ export function getPackageLeaks(packedPaths) {
|
|
|
418
432
|
});
|
|
419
433
|
}
|
|
420
434
|
|
|
435
|
+
/** `src/x.css` in a workspace package → `packages/web-x/src/x.css`; unchanged at the repo root. */
|
|
436
|
+
function getPackageFilePath(checkedPackage, filePath) {
|
|
437
|
+
return checkedPackage.path === '.' ? filePath : `${checkedPackage.path.replace(/\\/g, '/').replace(/\/$/, '')}/${filePath}`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** The workspace directories package.json declares (`packages/*` → each subdirectory with a package.json). */
|
|
441
|
+
function getWorkspaceRoots(config) {
|
|
442
|
+
const workspaces = config.packageJson.workspaces;
|
|
443
|
+
const workspacePatterns = Array.isArray(workspaces) ? workspaces : workspaces?.packages || [];
|
|
444
|
+
return workspacePatterns.flatMap((pattern) => {
|
|
445
|
+
if (!pattern.endsWith('/*')) return [resolve(config.root, pattern)];
|
|
446
|
+
const parentPath = resolve(config.root, pattern.slice(0, -2));
|
|
447
|
+
if (!existsSync(parentPath)) return [];
|
|
448
|
+
return readdirSync(parentPath)
|
|
449
|
+
.map((entryName) => join(parentPath, entryName))
|
|
450
|
+
.filter((entryPath) => existsSync(join(entryPath, 'package.json')));
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* A monorepo lists its packages in kmwc.packages, or doctor checks only the root (which a
|
|
456
|
+
* private workspace root never publishes). Every workspace package is listed; every listed one exists.
|
|
457
|
+
*/
|
|
458
|
+
function checkWorkspacePackages(config, { addPass, addWarning, addError }) {
|
|
459
|
+
for (const checkedPackage of config.packages) {
|
|
460
|
+
if (!existsSync(join(checkedPackage.root, 'package.json'))) addError('packages', `kmwc.packages lists ${checkedPackage.path}, which has no package.json`, 'fix the path');
|
|
461
|
+
}
|
|
462
|
+
const workspaceRoots = getWorkspaceRoots(config);
|
|
463
|
+
if (!workspaceRoots.length) return;
|
|
464
|
+
const toRepoPath = (absolutePath) => relative(config.root, absolutePath).replace(/\\/g, '/');
|
|
465
|
+
if (!config.packageJson.kmwc?.packages) {
|
|
466
|
+
addWarning(
|
|
467
|
+
'packages',
|
|
468
|
+
`npm workspaces (${workspaceRoots.map(toRepoPath).join(', ')}) but no kmwc.packages — doctor checks the root as the package`,
|
|
469
|
+
`list them: "packages": [${workspaceRoots.map((workspaceRoot) => `{ "path": "${toRepoPath(workspaceRoot)}", "kind": "component" | "library" }`).join(', ')}]`
|
|
470
|
+
);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const listedRoots = new Set(config.packages.map((checkedPackage) => checkedPackage.root));
|
|
474
|
+
const unlistedRoots = workspaceRoots.filter((workspaceRoot) => !listedRoots.has(workspaceRoot));
|
|
475
|
+
for (const workspaceRoot of unlistedRoots) {
|
|
476
|
+
addWarning('packages', `workspace package ${toRepoPath(workspaceRoot)} is not in kmwc.packages — doctor doesn't check it`, `add { "path": "${toRepoPath(workspaceRoot)}", "kind": … }`);
|
|
477
|
+
}
|
|
478
|
+
if (!unlistedRoots.length) addPass('packages', `kmwc.packages lists every workspace package (${config.packages.map((checkedPackage) => `${checkedPackage.path} ${checkedPackage.kind}`).join(', ')})`);
|
|
479
|
+
}
|
|
480
|
+
|
|
421
481
|
/** The file list `npm publish` would ship (no lifecycle scripts, nothing written). */
|
|
422
|
-
function getPackedPaths(
|
|
482
|
+
function getPackedPaths(packageRoot) {
|
|
423
483
|
const packResult = spawnSync('npm', ['pack', '--dry-run', '--json', '--ignore-scripts'], {
|
|
424
|
-
cwd:
|
|
484
|
+
cwd: packageRoot,
|
|
425
485
|
encoding: 'utf8',
|
|
426
486
|
shell: process.platform === 'win32',
|
|
427
487
|
});
|
|
@@ -430,26 +490,37 @@ function getPackedPaths(config) {
|
|
|
430
490
|
return packReport[0].files.map((file) => file.path);
|
|
431
491
|
}
|
|
432
492
|
|
|
433
|
-
|
|
493
|
+
/**
|
|
494
|
+
* One package's `npm pack` list. A private workspace root is skipped: npm never publishes it and,
|
|
495
|
+
* having no `files`, it would "leak" everything (list the packages in kmwc.packages). A private
|
|
496
|
+
* single package (the devkit playground) is still checked — it stands in for a real one.
|
|
497
|
+
*/
|
|
498
|
+
function checkPackageContents(checkedPackage, { addPass, addWarning, addError }) {
|
|
499
|
+
const packageLabel = checkedPackage.path === '.' ? '' : `${checkedPackage.path}: `;
|
|
500
|
+
const packageJson = JSON.parse(readFileSync(join(checkedPackage.root, 'package.json'), 'utf8'));
|
|
501
|
+
if (packageJson.private && packageJson.workspaces) {
|
|
502
|
+
addPass('package', `${packageLabel}private workspace root — never published, contents not checked`);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
434
505
|
let packedPaths;
|
|
435
506
|
try {
|
|
436
|
-
packedPaths = getPackedPaths(
|
|
507
|
+
packedPaths = getPackedPaths(checkedPackage.root);
|
|
437
508
|
} catch {
|
|
438
509
|
packedPaths = null;
|
|
439
510
|
}
|
|
440
511
|
if (!packedPaths) {
|
|
441
|
-
addWarning('package',
|
|
512
|
+
addWarning('package', `${packageLabel}npm pack --dry-run failed — package contents not checked`);
|
|
442
513
|
return;
|
|
443
514
|
}
|
|
444
515
|
const packageLeaks = getPackageLeaks(packedPaths);
|
|
445
516
|
for (const packageLeak of packageLeaks) {
|
|
446
517
|
addError(
|
|
447
518
|
'package',
|
|
448
|
-
`${packageLeak.path} would be published (${packageLeak.label})`,
|
|
449
|
-
`exclude it in package.json "files" (e.g. ${PACKAGE_FILES_EXCLUSIONS.map((exclusion) => `"${exclusion}"`).join(', ')}) or narrow the glob`
|
|
519
|
+
`${getPackageFilePath(checkedPackage, packageLeak.path)} would be published (${packageLeak.label})`,
|
|
520
|
+
`exclude it in ${packageLabel ? `${checkedPackage.path}/` : ''}package.json "files" (e.g. ${PACKAGE_FILES_EXCLUSIONS.map((exclusion) => `"${exclusion}"`).join(', ')}) or narrow the glob`
|
|
450
521
|
);
|
|
451
522
|
}
|
|
452
|
-
if (!packageLeaks.length) addPass('package', `${packedPaths.length} packed file(s), no dev-only files`);
|
|
523
|
+
if (!packageLeaks.length) addPass('package', `${packageLabel}${packedPaths.length} packed file(s), no dev-only files`);
|
|
453
524
|
}
|
|
454
525
|
|
|
455
526
|
function checkRulebook(config, { addPass, addWarning, addError }) {
|
|
@@ -458,21 +529,32 @@ function checkRulebook(config, { addPass, addWarning, addError }) {
|
|
|
458
529
|
return;
|
|
459
530
|
}
|
|
460
531
|
const skippedSuites = new Set(config.rulebook.skippedChecks);
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
532
|
+
const isMonorepo = config.packages.some((checkedPackage) => checkedPackage.path !== '.');
|
|
533
|
+
// [{ suite, checkedRoot, checkName, acceptedCheckIds }]: a package's suites run in the package
|
|
534
|
+
// (for its kind), the repo-wide ones once at the root. A single-package repo runs all at the root.
|
|
535
|
+
const suiteRuns = [];
|
|
536
|
+
for (const checkedPackage of config.packages) {
|
|
537
|
+
const suites = getRulebookSuites(checkedPackage.kind).filter((suite) => !isMonorepo || !suite.isRepoWide);
|
|
538
|
+
const packageSuffix = isMonorepo ? `@${checkedPackage.path}` : '';
|
|
539
|
+
if (suites.length < RULEBOOK_SUITES.filter((suite) => !isMonorepo || !suite.isRepoWide).length) {
|
|
540
|
+
const componentOnlySuiteNames = RULEBOOK_SUITES.filter((suite) => suite.isComponentOnly).map((suite) => suite.name);
|
|
541
|
+
addPass(`rulebook${packageSuffix}`, `${checkedPackage.kind}: component-only suites not run (${componentOnlySuiteNames.join(', ')})`);
|
|
542
|
+
}
|
|
543
|
+
const acceptedCheckIds = new Set([...loadAcceptedCheckIds(config.root), ...loadAcceptedCheckIds(checkedPackage.root)]);
|
|
544
|
+
for (const suite of suites) suiteRuns.push({ suite, checkedRoot: checkedPackage.root, checkName: `rulebook:${suite.name}${packageSuffix}`, acceptedCheckIds });
|
|
545
|
+
}
|
|
546
|
+
if (isMonorepo) {
|
|
547
|
+
const acceptedCheckIds = loadAcceptedCheckIds(config.root);
|
|
548
|
+
for (const suite of RULEBOOK_SUITES.filter((repoSuite) => repoSuite.isRepoWide)) suiteRuns.push({ suite, checkedRoot: config.root, checkName: `rulebook:${suite.name}`, acceptedCheckIds });
|
|
466
549
|
}
|
|
467
|
-
for (const suite of
|
|
550
|
+
for (const { suite, checkedRoot, checkName, acceptedCheckIds } of suiteRuns) {
|
|
468
551
|
if (skippedSuites.has(suite.name)) continue;
|
|
469
552
|
if (!existsSync(join(config.rulebook.path, `${suite.name}.checks.sh`))) continue;
|
|
470
|
-
const suiteResult = runRulebookSuite(config, suite);
|
|
553
|
+
const suiteResult = runRulebookSuite(config, suite, checkedRoot);
|
|
471
554
|
if (suiteResult.error) {
|
|
472
555
|
addWarning('rulebook', `cannot run bash (${suiteResult.error.code}) — rulebook checks skipped`, 'install Git Bash / run from a POSIX shell');
|
|
473
556
|
return;
|
|
474
557
|
}
|
|
475
|
-
const checkName = `rulebook:${suite.name}`;
|
|
476
558
|
const failures = getSuiteFailures(suiteResult.stdout);
|
|
477
559
|
// A suite passes with exit 0 and fails with exit 1 + FAIL lines. Anything else (2 = usage,
|
|
478
560
|
// 127 = the script wasn't found — WSL's bash on a C:/ path) means it never ran: say so,
|
package/src/config.d.ts
CHANGED
|
@@ -73,6 +73,19 @@ export interface KmwcPackageConfig {
|
|
|
73
73
|
skippedChecks?: string[];
|
|
74
74
|
};
|
|
75
75
|
sync?: { skippedFiles?: string[] };
|
|
76
|
+
/**
|
|
77
|
+
* The packages this repo publishes, for a monorepo whose root is not the package (npm
|
|
78
|
+
* workspaces): doctor runs the package-contents check and the rulebook suites for each one's
|
|
79
|
+
* kind there. Default: one package, the repo root, with the repo's kind.
|
|
80
|
+
*/
|
|
81
|
+
packages?: Array<{ path: string; kind: 'component' | 'library' }>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** One package doctor checks: `path` as configured, `root` resolved. */
|
|
85
|
+
export interface KmwcPackage {
|
|
86
|
+
path: string;
|
|
87
|
+
root: string;
|
|
88
|
+
kind: 'component' | 'library';
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
export interface KmwcConfig {
|
|
@@ -131,6 +144,7 @@ export interface KmwcConfig {
|
|
|
131
144
|
};
|
|
132
145
|
rulebook: { path: string; skippedChecks: string[] };
|
|
133
146
|
sync: { skippedFiles: string[] };
|
|
147
|
+
packages: KmwcPackage[];
|
|
134
148
|
}
|
|
135
149
|
|
|
136
150
|
export const DEVKIT_ROOT: string;
|
package/src/config.js
CHANGED
|
@@ -183,6 +183,23 @@ export function getKmwcConfig(root = process.cwd()) {
|
|
|
183
183
|
return mapKmwcConfig(getJsonFile(packageJsonPath), root);
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
const PACKAGE_KINDS = ['component', 'library'];
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* `kmwc.packages` → [{ path, root, kind }]; without it, one package at the repo root with the
|
|
190
|
+
* repo's kind (a showcase / docs / test repo checks as a component, as before).
|
|
191
|
+
*/
|
|
192
|
+
function mapPackages(packageEntries, root, repoKind) {
|
|
193
|
+
if (!packageEntries) return [{ path: '.', root, kind: repoKind === 'library' ? 'library' : 'component' }];
|
|
194
|
+
if (!Array.isArray(packageEntries) || !packageEntries.length) throw new Error('kmwc.packages: expected a non-empty array of { path, kind }');
|
|
195
|
+
return packageEntries.map((packageEntry) => {
|
|
196
|
+
if (!packageEntry?.path || !PACKAGE_KINDS.includes(packageEntry.kind)) {
|
|
197
|
+
throw new Error(`kmwc.packages: ${JSON.stringify(packageEntry)} needs a "path" and a "kind" (${PACKAGE_KINDS.join(' | ')})`);
|
|
198
|
+
}
|
|
199
|
+
return { path: packageEntry.path, root: resolve(root, packageEntry.path), kind: packageEntry.kind };
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
186
203
|
/** Map a parsed package.json to the resolved config (pure — used by tests). */
|
|
187
204
|
export function mapKmwcConfig(packageJson, root = process.cwd()) {
|
|
188
205
|
const packageConfig = packageJson.kmwc || {};
|
|
@@ -199,6 +216,7 @@ export function mapKmwcConfig(packageJson, root = process.cwd()) {
|
|
|
199
216
|
const cssVariables = packageConfig.cssVariables === false ? { checkMode: 'off' } : packageConfig.cssVariables || {};
|
|
200
217
|
const rulebook = packageConfig.rulebook || {};
|
|
201
218
|
const devPort = ports.dev ?? 5173;
|
|
219
|
+
const kind = packageConfig.kind || getRegistryKind(id);
|
|
202
220
|
|
|
203
221
|
return {
|
|
204
222
|
root,
|
|
@@ -206,7 +224,7 @@ export function mapKmwcConfig(packageJson, root = process.cwd()) {
|
|
|
206
224
|
packageJson,
|
|
207
225
|
id,
|
|
208
226
|
packageName: packageJson.name || id,
|
|
209
|
-
kind
|
|
227
|
+
kind,
|
|
210
228
|
version: packageJson.version || '0.1.0',
|
|
211
229
|
title: packageConfig.title || mapToTitleCase(id),
|
|
212
230
|
tagline: packageConfig.tagline || packageJson.description || '',
|
|
@@ -275,6 +293,7 @@ export function mapKmwcConfig(packageJson, root = process.cwd()) {
|
|
|
275
293
|
sync: {
|
|
276
294
|
skippedFiles: packageConfig.sync?.skippedFiles || [],
|
|
277
295
|
},
|
|
296
|
+
packages: mapPackages(packageConfig.packages, root, kind),
|
|
278
297
|
};
|
|
279
298
|
}
|
|
280
299
|
|
|
@@ -97,6 +97,12 @@ export const INDEX_PAGE_CLASSES = {
|
|
|
97
97
|
devkit: 'kmwc-index__devkit',
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
+
/** The footer the kit appends to every example page but the index (page-footer.js). */
|
|
101
|
+
export const PAGE_FOOTER_CLASSES = {
|
|
102
|
+
root: 'kmwc-page-footer',
|
|
103
|
+
devkit: 'kmwc-page-footer__devkit',
|
|
104
|
+
};
|
|
105
|
+
|
|
100
106
|
/** <kmwc-demo> output (rendered at build time by page-composer.js) + the kit's copy button. */
|
|
101
107
|
export const DEMO_CLASSES = {
|
|
102
108
|
root: 'kmwc-demo',
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* ==============================================================================
|
|
2
|
+
EXAMPLES KIT — PAGE FOOTER
|
|
3
|
+
==============================================================================
|
|
4
|
+
The footer the kit appends to every example page but the index
|
|
5
|
+
(page-footer.js); the same line as the index footer. */
|
|
6
|
+
@layer kmwc {
|
|
7
|
+
.kmwc-page-footer {
|
|
8
|
+
text-align: center;
|
|
9
|
+
margin-top: 3rem;
|
|
10
|
+
padding: 1.5rem 0 1rem;
|
|
11
|
+
border-top: 1px solid var(--kmwc-border-color);
|
|
12
|
+
color: var(--kmwc-text-color-3);
|
|
13
|
+
font-size: 0.875rem;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.kmwc-page-footer p {
|
|
17
|
+
margin: 0;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { getPageConfig } from './page-config.js';
|
|
20
20
|
import { getVersionLabel } from './version-badge.js';
|
|
21
21
|
import { INDEX_PAGE_CLASSES } from './constants.js';
|
|
22
|
+
import { renderFooterLineHtml } from './page-footer.js';
|
|
22
23
|
|
|
23
24
|
const HTML_ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
24
25
|
const mapToEscapedHtml = (text) => String(text ?? '').replace(/[&<>"']/g, (character) => HTML_ESCAPES[character]);
|
|
@@ -32,10 +33,6 @@ export function renderExamplesIndex(indexConfig) {
|
|
|
32
33
|
const pages = indexConfig.pages || [];
|
|
33
34
|
const title = indexConfig.title ?? pageConfig.title;
|
|
34
35
|
const tagline = indexConfig.tagline ?? pageConfig.tagline;
|
|
35
|
-
const footerLinks = [
|
|
36
|
-
...(pageConfig.repository ? [{ label: 'GitHub', href: pageConfig.repository }] : []),
|
|
37
|
-
...(indexConfig.links || []),
|
|
38
|
-
];
|
|
39
36
|
const classes = INDEX_PAGE_CLASSES;
|
|
40
37
|
|
|
41
38
|
document.body.classList.add(classes.body);
|
|
@@ -63,13 +60,7 @@ export function renderExamplesIndex(indexConfig) {
|
|
|
63
60
|
.join('')}
|
|
64
61
|
</div>
|
|
65
62
|
<footer class="${classes.footer}">
|
|
66
|
-
|
|
67
|
-
.map((link) => ` | <a href="${mapToEscapedHtml(link.href)}" target="_blank" rel="noopener">${mapToEscapedHtml(link.label)}</a>`)
|
|
68
|
-
.join('')}${
|
|
69
|
-
pageConfig.devkitVersion
|
|
70
|
-
? ` | <a class="${classes.devkit}" href="${mapToEscapedHtml(pageConfig.devkitHomepage)}" target="_blank" rel="noopener">web-components-devkit v${mapToEscapedHtml(pageConfig.devkitVersion)}</a>`
|
|
71
|
-
: ''
|
|
72
|
-
}</p>
|
|
63
|
+
${renderFooterLineHtml(pageConfig, { links: indexConfig.links, devkitClass: classes.devkit })}
|
|
73
64
|
</footer>`;
|
|
74
65
|
containerEl.prepend(pageElement);
|
|
75
66
|
|
package/src/examples/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface KmwcPageConfig {
|
|
|
11
11
|
isSpotlightEnabled: boolean;
|
|
12
12
|
repository: string;
|
|
13
13
|
homepage: string;
|
|
14
|
-
/** The @keenmate/web-components-devkit version that built the page (shown in the
|
|
14
|
+
/** The @keenmate/web-components-devkit version that built the page (shown in the page footers). */
|
|
15
15
|
devkitVersion: string;
|
|
16
16
|
devkitHomepage: string;
|
|
17
17
|
}
|
|
@@ -93,6 +93,10 @@ export function renderVersionBadges(root?: ParentNode): void;
|
|
|
93
93
|
export function attachDemoCopyButtons(root?: ParentNode): void;
|
|
94
94
|
export function getVersionLabel(pageConfig?: KmwcPageConfig): string;
|
|
95
95
|
|
|
96
|
+
/** Append the footer (Keenmate | GitHub | devkit version) to an example page; none on the index. */
|
|
97
|
+
export function mountPageFooter(): HTMLElement | null;
|
|
98
|
+
/** The footer line the index and the page footer share. */
|
|
99
|
+
export function renderFooterLineHtml(pageConfig: KmwcPageConfig, footerConfig: { links?: Array<{ label: string; href: string }>; devkitClass: string }): string;
|
|
96
100
|
export function mountThemeSwitch(): { unmount(): void };
|
|
97
101
|
export function applyTheme(theme: Theme): void;
|
|
98
102
|
export function getSavedTheme(): Theme;
|
|
@@ -118,6 +122,7 @@ export const SPOTLIGHT_FULLSCREEN_MEDIA: string;
|
|
|
118
122
|
export const CHAPTER_CODE_PATTERN: RegExp;
|
|
119
123
|
export const COPY_LINK_CLASSES: Record<string, string>;
|
|
120
124
|
export const INDEX_PAGE_CLASSES: Record<string, string>;
|
|
125
|
+
export const PAGE_FOOTER_CLASSES: { root: string; devkit: string };
|
|
121
126
|
export const THEME_SWITCH_CLASSES: Record<string, string>;
|
|
122
127
|
export const DEMO_CLASSES: Record<string, string>;
|
|
123
128
|
export const THEMES: Theme[];
|
package/src/examples/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export { searchChapters, checkMatchesAny, renderHighlightedText } from './chapte
|
|
|
15
15
|
export { attachCopyLinks, copyText, getSectionUrl } from './copy-link.js';
|
|
16
16
|
export { renderVersionBadges, getVersionLabel } from './version-badge.js';
|
|
17
17
|
export { attachDemoCopyButtons } from './demo-code.js';
|
|
18
|
+
export { mountPageFooter, renderFooterLineHtml } from './page-footer.js';
|
|
18
19
|
export { mountThemeSwitch, applyTheme, getSavedTheme } from './theme-switch.js';
|
|
19
20
|
export { ensureSectionAnchors, generateSlug, getHeadingLabel, getChapterCode, mapToCodeKey, findSectionByHash } from './sections.js';
|
|
20
21
|
export { getPageConfig, attachDomReadyHandler } from './page-config.js';
|
package/src/examples/kit.js
CHANGED
|
@@ -5,15 +5,16 @@ import { attachCopyLinks } from './copy-link.js';
|
|
|
5
5
|
import { renderVersionBadges } from './version-badge.js';
|
|
6
6
|
import { mountThemeSwitch } from './theme-switch.js';
|
|
7
7
|
import { attachDemoCopyButtons } from './demo-code.js';
|
|
8
|
+
import { mountPageFooter } from './page-footer.js';
|
|
8
9
|
import { INDEX_PAGE_CLASSES } from './constants.js';
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* (Re)mount the page-level helpers on the current DOM (copy-links, chapter
|
|
12
13
|
* nav, short chapter links, spotlight, version badges, demo copy buttons,
|
|
13
|
-
* theme switch). Runs automatically once per page; call it again after
|
|
14
|
+
* theme switch, page footer). Runs automatically once per page; call it again after
|
|
14
15
|
* rendering sections asynchronously. Idempotent: copy-link / demo copy buttons
|
|
15
|
-
* skip elements that already have one, the nav, spotlight
|
|
16
|
-
* remounted.
|
|
16
|
+
* skip elements that already have one, the nav, spotlight, theme switch and
|
|
17
|
+
* footer are remounted.
|
|
17
18
|
*/
|
|
18
19
|
export function mountExamplesKit() {
|
|
19
20
|
// Copy-links before the nav: the nav strips the buttons back out when
|
|
@@ -26,4 +27,5 @@ export function mountExamplesKit() {
|
|
|
26
27
|
renderVersionBadges();
|
|
27
28
|
attachDemoCopyButtons();
|
|
28
29
|
mountThemeSwitch();
|
|
30
|
+
mountPageFooter();
|
|
29
31
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The footer every example page ends with — Keenmate, the repo, and the devkit
|
|
3
|
+
* that built the page. The index page renders the same line in its own footer
|
|
4
|
+
* (index-page.js); the other pages get it from the kit (mountPageFooter).
|
|
5
|
+
*/
|
|
6
|
+
import { getPageConfig } from './page-config.js';
|
|
7
|
+
import { INDEX_PAGE_CLASSES, PAGE_FOOTER_CLASSES } from './constants.js';
|
|
8
|
+
|
|
9
|
+
const HTML_ESCAPES = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
|
10
|
+
const mapToEscapedHtml = (text) => String(text ?? '').replace(/[&<>"']/g, (character) => HTML_ESCAPES[character]);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The footer line: "Created by Keenmate s.r.o. | GitHub | …links | web-components-devkit vX".
|
|
14
|
+
* @param {import('./index.js').KmwcPageConfig} pageConfig
|
|
15
|
+
* @param {{ links?: Array<{ label: string, href: string }>, devkitClass: string }} footerConfig
|
|
16
|
+
*/
|
|
17
|
+
export function renderFooterLineHtml(pageConfig, { links = [], devkitClass }) {
|
|
18
|
+
const footerLinks = [...(pageConfig.repository ? [{ label: 'GitHub', href: pageConfig.repository }] : []), ...links];
|
|
19
|
+
const linksHtml = footerLinks
|
|
20
|
+
.map((link) => ` | <a href="${mapToEscapedHtml(link.href)}" target="_blank" rel="noopener">${mapToEscapedHtml(link.label)}</a>`)
|
|
21
|
+
.join('');
|
|
22
|
+
const devkitHtml = pageConfig.devkitVersion
|
|
23
|
+
? ` | <a class="${devkitClass}" href="${mapToEscapedHtml(pageConfig.devkitHomepage)}" target="_blank" rel="noopener">web-components-devkit v${mapToEscapedHtml(pageConfig.devkitVersion)}</a>`
|
|
24
|
+
: '';
|
|
25
|
+
return `<p>Created by <a href="https://github.com/keenmate" target="_blank" rel="noopener">Keenmate s.r.o.</a>${linksHtml}${devkitHtml}</p>`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Append the footer to an example page — inside the layout's `.container`
|
|
30
|
+
* when there is one, else at the end of <body>. Remounting replaces it; the
|
|
31
|
+
* index page keeps its own footer and gets none.
|
|
32
|
+
* @returns {HTMLElement | null}
|
|
33
|
+
*/
|
|
34
|
+
export function mountPageFooter() {
|
|
35
|
+
document.querySelector(`.${PAGE_FOOTER_CLASSES.root}`)?.remove();
|
|
36
|
+
if (document.body.classList.contains(INDEX_PAGE_CLASSES.body)) return null;
|
|
37
|
+
|
|
38
|
+
const footerElement = document.createElement('footer');
|
|
39
|
+
footerElement.className = PAGE_FOOTER_CLASSES.root;
|
|
40
|
+
footerElement.innerHTML = renderFooterLineHtml(getPageConfig(), { devkitClass: PAGE_FOOTER_CLASSES.devkit });
|
|
41
|
+
const containerElement = document.querySelector('body > .container');
|
|
42
|
+
(containerElement || document.body).append(footerElement);
|
|
43
|
+
return footerElement;
|
|
44
|
+
}
|