@keenmate/web-components-devkit 1.0.0-rc08 → 1.0.0-rc09
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 +21 -0
- package/README.md +11 -10
- package/package.json +1 -1
- package/src/cli/commands/check-pages.js +75 -10
- package/src/examples/css/controls.css +2 -2
- package/src/examples/css/tokens.css +6 -1
- package/templates/claude-block.md +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.0-rc09] - 2026-11-04 [PUBLISHED]
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **check-pages: page CSS that only matches inside a shadow root.** A page's own rules (its `<style>`
|
|
8
|
+
blocks and `examples-local.css`) are matched against the loaded page. A rule whose element (the last
|
|
9
|
+
compound, pseudo-classes dropped) exists nowhere in the document but inside a component's shadow root
|
|
10
|
+
never applies — page CSS stops at the shadow boundary. web-treeview styled the rows its
|
|
11
|
+
`renderNodeCallback` draws that way (custom-layout: `.file-icon`, `.node-badge`, `.badge-new` …;
|
|
12
|
+
responsive: `.isco-row`, `.dev-badge` …, 18 rules); against its pre-fix pages the check reports
|
|
13
|
+
exactly those. Checked once per page (first width).
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **The grey secondary button in dark mode** (`button.secondary` / `.btn-secondary`) had white text on
|
|
18
|
+
`#9ca3af` at 2.54:1. That grey sits just under the on-colour formula's 0.34 luminance switch, so the
|
|
19
|
+
formula picked white. It showed on 9 web-treeview pages ("Clear", "Reset", "Move up" …) and on every
|
|
20
|
+
other site's grey buttons. The button now has its own fill, `--kmwc-neutral-fill-color`
|
|
21
|
+
(`#6b7280` light / `#4b5563` dark, white text ≥ 4.5:1 in both, hovered too), and
|
|
22
|
+
`--kmwc-neutral-fill-color-hover`. Muted dots and pills keep the light `--kmwc-neutral-color`.
|
|
23
|
+
|
|
3
24
|
## [1.0.0-rc08] - 2026-11-04 [PUBLISHED]
|
|
4
25
|
|
|
5
26
|
### Added
|
package/README.md
CHANGED
|
@@ -19,19 +19,16 @@ 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-rc09
|
|
23
|
+
|
|
24
|
+
- **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`).
|
|
25
|
+
- **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.
|
|
26
|
+
|
|
22
27
|
## What's New in v1.0.0-rc08
|
|
23
28
|
|
|
24
29
|
- **Doctor — hard-coded colours in example pages** — web-treeview's devkit migration kept each page's own `<style>` block, and on 8 of its 14 pages the search bars, status bars, badges and panels kept their light colours when the theme switch went dark. rc07 only flagged page CSS that restyles a kit class, so nothing reported them. Doctor now warns about a colour literal (`#hex`, `rgb()` / `hsl()` / `oklch()`, a named colour) on any colour property in a page `<style>`, a `style="…"` attribute or `examples-local.css`. `var()` fallbacks, `light-dark()` pairs, custom properties and `rgb(from var(…))` pass, because they follow the theme. A block that shows fixed colours on purpose, such as a theme gallery or a mockup, opts out with `<style data-kmwc-fixed-colors>`, and a single rule with a `/* kmwc-fixed-colors: why */` comment before it.
|
|
25
30
|
- **Doctor — the loader class matches the installed core** — Core 6d6b768 renamed its default loader's class from `<prefix>-loader` to `<prefix>__loader`, with no release that emits both. A component that renames its selector before the core bump, or forgets to after it, ships a loader with no styles. No rulebook check sees that, and a `## C-NC-8` heading in VALIDATION-NOTES.md would hide a leftover `.x-loader` from the BEM check. Doctor reads which class the installed core's `dist/dom/render-loader.js` draws and fails on any selector in the component's `src/**/*.css` that styles the other one. The error names the file and says which side should change.
|
|
26
31
|
|
|
27
|
-
## What's New in v1.0.0-rc07
|
|
28
|
-
|
|
29
|
-
- **CLI — `kmwc check-pages` finds what only a browser can see** — Migrating web-treeview turned up pages that pushed a phone sideways: an iOS-Files mockup 420px wide and a theme gallery built on `minmax(500px, 1fr)`. No static check could have caught that. `kmwc check-pages` loads every example page in Chromium at 1440, 390 and 320px. It reports page and console errors, failed requests, custom elements that never get defined, and pages wider than the viewport, naming the widest elements. It looks inside shadow roots and at long words that overflow their box. It ignores layers that can't widen a page: scrolling code blocks, hidden or fixed layers, a closed chapter-nav panel. It starts the repo's dev server, or `--url` points it at one already running (for example the built site on the preview port), and `--widths` changes the widths. `kmwc sync` adds a `check:pages` script and a `make check-pages` target.
|
|
30
|
-
- **Doctor — catches what the treeview and daterangepicker migrations got wrong** — Doctor now checks that every `./dist/…` target in package.json is a file the library build actually emits, and that the old UMD name matches `kmwc.library.globalName`. Without that, the preset's defaults would quietly have renamed treeview's global (`WebTreeView` → `WebTreeview`) and its CSS (`web-treeview.css` → `style.css`). Doctor also reports scripts that pin their own `--port`, config, Makefile and doc ports outside the repo's registry block, and sibling repos that listen inside it. This found web-daterangepicker on treeview's 12300 and web-multiselect / web-dropzone on daterangepicker's 12200. A Plausible site id that another repo also uses is an error, because treeview's traffic was being counted as web-multiselect's. Page CSS that duplicates kit classes (`.kbd`, `mark`, inline field widths, `margin-left: auto`) is a warning.
|
|
31
|
-
- **Doctor — `VALIDATION-NOTES.md` records accepted deviations** — The rulebook lets a component record an architectural decision instead of following a rule. Doctor still reported those as errors (treeview's C-CST-4, C-CSS-1 and C-NC-6). A rulebook check id used as a `##` heading in `VALIDATION-NOTES.md` (`## C-CST-4 / C-CST-10 — …`) now counts as a pass, and `--verbose` lists them. A mention in plain prose doesn't count.
|
|
32
|
-
- **check-vars — input variables are not dead** — Some component variables are public knobs that are read with a fallback and never declared. Treeview forwards `--wtv-context-menu-*` to core's private `--_km-menu-*` so a page-wide `--km-menu-*` still applies, and declaring them would break that chain. check-vars called all 26 of them dead. In the default mode a variable now counts as present when it is declared or read, and a read-with-fallback missing from the manifest is reported as undocumented. `cssVariables.sources` also takes a package path (`@keenmate/web-components-core/dist/menu/styles.js`) for the `--base-*` reads of a core module the component renders.
|
|
33
|
-
- **Example pages — fit a 320px phone, and injected scripts land in the right place** — A chapter heading that names an API outside `<code>` now breaks instead of widening the page. On a phone the index title is 2rem. Below 400px, body, card and demo padding shrink, so a 250px inline calendar fits inside a card. `mark` no longer splits a word. The Vite plugin now puts its head block after the real `<head>` tag (not `<header>` or the text "<head>"). It puts the kit script before the last `</body>`, so it can no longer land inside a page's inline script and break it. That's the failure web-multiselect's sed-based Dockerfile caused on its live action-buttons page.
|
|
34
|
-
|
|
35
32
|
## Catalogue
|
|
36
33
|
|
|
37
34
|
<!-- kmwc:catalogue:start — generated by `npm run readme`, do not edit inside -->
|
|
@@ -365,7 +362,7 @@ kmwc doctor [--json] devkit checks + package contents + the rulebook's
|
|
|
365
362
|
kmwc check-vars [--strict] component-variables.manifest.json ⇔ CSS
|
|
366
363
|
kmwc check-pages [--url u] every example page in Chromium at 1440 / 390 / 320px: page and console
|
|
367
364
|
errors, failed requests, never-defined elements, a page wider than the
|
|
368
|
-
viewport. Starts the dev server unless --url (e.g. the built site);
|
|
365
|
+
viewport, page CSS that only matches inside a shadow root. Starts the dev server unless --url (e.g. the built site);
|
|
369
366
|
--widths 1440,390,320
|
|
370
367
|
kmwc check-release --tag t latest ⇔ release version, rc ⇔ -rcN, version not yet on npm
|
|
371
368
|
kmwc new web-<name> scaffold a component (--prefix, --title, --tag, --dir, --port, --minimal, --register)
|
|
@@ -427,7 +424,11 @@ generated files:
|
|
|
427
424
|
|
|
428
425
|
`kmwc check-pages` is the browser half: it loads every example page and reports
|
|
429
426
|
what doctor can't see in the source — a script error, a 404, a fixed width that
|
|
430
|
-
pushes a phone sideways
|
|
427
|
+
pushes a phone sideways, and page CSS that can never apply: a rule (in the page's
|
|
428
|
+
`<style>` or `examples-local.css`) whose element exists only inside a component's
|
|
429
|
+
shadow root, such as the rows a `renderNodeCallback` draws. Page CSS stops at the
|
|
430
|
+
shadow boundary — style those through a CSS variable, `::part` or the component's
|
|
431
|
+
styles hook (`getCustomStylesCallback`).
|
|
431
432
|
|
|
432
433
|
**Package contents.** Doctor runs `npm pack --dry-run` and fails on anything
|
|
433
434
|
dev-only that would reach a consumer's `node_modules`: `CLAUDE.md` at any
|
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-rc09",
|
|
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",
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* kmwc check-pages — load every example page in a real browser and report what
|
|
3
3
|
* a reader would hit: page errors, console errors, failed requests, custom
|
|
4
|
-
* elements that never get defined,
|
|
5
|
-
* width or a long word that pushes a phone sideways)
|
|
4
|
+
* elements that never get defined, a page wider than the viewport (a fixed
|
|
5
|
+
* width or a long word that pushes a phone sideways), and page CSS that can
|
|
6
|
+
* never apply because what it targets lives only inside a shadow root (classes
|
|
7
|
+
* a render callback puts into the component).
|
|
6
8
|
*
|
|
7
9
|
* Runs against the repo's own dev server (started here on kmwc.ports.dev), or a
|
|
8
10
|
* server you already have (--url, e.g. the built site on the preview port).
|
|
9
11
|
* Widths default to desktop + two phones. Uses the component's own
|
|
10
12
|
* @playwright/test (see getPeerModule) — `npx playwright install chromium` once.
|
|
11
13
|
*/
|
|
12
|
-
import { existsSync } from 'node:fs';
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
13
15
|
import { join } from 'node:path';
|
|
14
16
|
import { getKmwcConfig, getPeerModule, loadPeerModule } from '../../config.js';
|
|
15
17
|
import { getExamplePages } from '../../vite/index.js';
|
|
18
|
+
import { getRuleSelectors } from '../repo-checks.js';
|
|
16
19
|
|
|
17
20
|
export const DEFAULT_WIDTHS = [1440, 390, 320];
|
|
18
21
|
const VIEWPORT_HEIGHT = 900;
|
|
19
22
|
const SETTLE_MS = 600;
|
|
20
23
|
const VITE_CONFIG_FILES = ['vite.config.ts', 'vite.config.mts', 'vite.config.mjs', 'vite.config.js'];
|
|
21
24
|
const MAX_OFFENDERS = 3;
|
|
25
|
+
const LOCAL_STYLESHEET_FILE = 'examples-local.css';
|
|
22
26
|
// Requests a page doesn't control (favicon probes, analytics in a built site).
|
|
23
27
|
const IGNORED_REQUEST_PATTERN = /favicon\.ico|stats\.keenmate\.services|plausible/;
|
|
24
28
|
|
|
@@ -30,12 +34,68 @@ export function parseWidths(widthsText) {
|
|
|
30
34
|
return widths;
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
/**
|
|
38
|
+
* The element a selector styles, as a selector of its own: the last compound with
|
|
39
|
+
* pseudo-classes / -elements dropped (`.tree .badge:hover::after` → `.badge`), or
|
|
40
|
+
* null when there's no class / id / attribute to look for (`:root`, `div`, `from`). Pure.
|
|
41
|
+
*/
|
|
42
|
+
export function getSelectorSubject(selector) {
|
|
43
|
+
let depth = 0;
|
|
44
|
+
let start = 0;
|
|
45
|
+
for (let index = 0; index < selector.length; index++) {
|
|
46
|
+
const character = selector[index];
|
|
47
|
+
if (character === '(' || character === '[') depth++;
|
|
48
|
+
else if (character === ')' || character === ']') depth--;
|
|
49
|
+
else if (depth === 0 && /[\s>+~]/.test(character)) start = index + 1;
|
|
50
|
+
}
|
|
51
|
+
const subject = selector.slice(start).replace(/::?[\w-]+(\([^()]*\))?/g, '').trim();
|
|
52
|
+
return /[.#[]/.test(subject) ? subject : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A page's own CSS rules (its `<style>` blocks + examples-local.css) → [{ selector, subject }]. */
|
|
56
|
+
export function loadPageRules(root, pagePath) {
|
|
57
|
+
const pageText = readFileSync(pagePath, 'utf8');
|
|
58
|
+
const localStylesheetPath = join(root, LOCAL_STYLESHEET_FILE);
|
|
59
|
+
const styleText = [
|
|
60
|
+
...[...pageText.matchAll(/<style\b[^>]*>([\s\S]*?)<\/style>/gi)].map((match) => match[1]),
|
|
61
|
+
existsSync(localStylesheetPath) ? readFileSync(localStylesheetPath, 'utf8') : '',
|
|
62
|
+
].join('\n');
|
|
63
|
+
const selectors = [...new Set(getRuleSelectors(styleText))];
|
|
64
|
+
return selectors.map((selector) => ({ selector, subject: getSelectorSubject(selector) })).filter((rule) => rule.subject);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Runs in the page: page rules whose subject matches nothing in the document but
|
|
69
|
+
* something inside a shadow root → [{ selector, host }]. Page CSS stops at the
|
|
70
|
+
* shadow boundary, so such a rule never applies.
|
|
71
|
+
*/
|
|
72
|
+
export function findShadowOnlyRules(rules) {
|
|
73
|
+
const getShadowRoots = (root) =>
|
|
74
|
+
[...root.querySelectorAll('*')].filter((element) => element.shadowRoot).flatMap((element) => [element.shadowRoot, ...getShadowRoots(element.shadowRoot)]);
|
|
75
|
+
const shadowRoots = getShadowRoots(document);
|
|
76
|
+
const safeQuery = (root, selector) => {
|
|
77
|
+
try {
|
|
78
|
+
return root.querySelector(selector);
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
return rules.flatMap(({ selector, subject }) => {
|
|
84
|
+
if (safeQuery(document, subject)) return [];
|
|
85
|
+
const shadowRoot = shadowRoots.find((root) => safeQuery(root, subject));
|
|
86
|
+
return shadowRoot ? [{ selector, host: shadowRoot.host.localName }] : [];
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
33
90
|
/**
|
|
34
91
|
* Problems in one page load's report → [string]. Pure — exported for tests.
|
|
35
|
-
* @param {{ width: number, errors: string[], undefinedElements: string[], scrollWidth: number, clientWidth: number, offenders: string[] }} pageReport
|
|
92
|
+
* @param {{ width: number, errors: string[], undefinedElements: string[], scrollWidth: number, clientWidth: number, offenders: string[], shadowOnlyRules?: { selector: string, host: string }[] }} pageReport
|
|
36
93
|
*/
|
|
37
94
|
export function getPageProblems(pageReport) {
|
|
38
95
|
const problems = [...pageReport.errors];
|
|
96
|
+
for (const { selector, host } of pageReport.shadowOnlyRules || []) {
|
|
97
|
+
problems.push(`page CSS "${selector}" only matches inside <${host}>'s shadow root — page styles can't reach it (use a CSS variable, ::part or the component's styles hook)`);
|
|
98
|
+
}
|
|
39
99
|
if (pageReport.undefinedElements.length) problems.push(`never defined: <${pageReport.undefinedElements.join('>, <')}>`);
|
|
40
100
|
if (pageReport.scrollWidth > pageReport.clientWidth) {
|
|
41
101
|
const offenderText = pageReport.offenders.length ? ` — widest: ${pageReport.offenders.join(', ')}` : '';
|
|
@@ -106,7 +166,7 @@ function inspectPage(maxOffenders) {
|
|
|
106
166
|
};
|
|
107
167
|
}
|
|
108
168
|
|
|
109
|
-
async function loadPageReport(browser, pageUrl, width) {
|
|
169
|
+
async function loadPageReport(browser, pageUrl, width, pageRules = []) {
|
|
110
170
|
const page = await browser.newPage({ viewport: { width, height: VIEWPORT_HEIGHT } });
|
|
111
171
|
const errors = [];
|
|
112
172
|
page.on('pageerror', (error) => errors.push(`page error: ${error.message.split('\n')[0]}`));
|
|
@@ -123,7 +183,8 @@ async function loadPageReport(browser, pageUrl, width) {
|
|
|
123
183
|
await page.goto(pageUrl, { waitUntil: 'networkidle', timeout: 30_000 });
|
|
124
184
|
await page.waitForTimeout(SETTLE_MS);
|
|
125
185
|
const inspection = await page.evaluate(inspectPage, MAX_OFFENDERS);
|
|
126
|
-
|
|
186
|
+
const shadowOnlyRules = pageRules.length ? await page.evaluate(findShadowOnlyRules, pageRules) : [];
|
|
187
|
+
return { width, errors: [...new Set(errors)], ...inspection, shadowOnlyRules };
|
|
127
188
|
} catch (error) {
|
|
128
189
|
return { width, errors: [...new Set([...errors, `load failed: ${error.message.split('\n')[0]}`])], scrollWidth: 0, clientWidth: 0, offenders: [], undefinedElements: [] };
|
|
129
190
|
} finally {
|
|
@@ -154,7 +215,8 @@ async function startDevServer(config) {
|
|
|
154
215
|
export async function run(args) {
|
|
155
216
|
const config = getKmwcConfig(args.root);
|
|
156
217
|
const widths = parseWidths(args.widths);
|
|
157
|
-
const
|
|
218
|
+
const pagePaths = Object.values(getExamplePages(config));
|
|
219
|
+
const pageFiles = pagePaths.map((pagePath) => pagePath.replace(/\\/g, '/').split('/').pop());
|
|
158
220
|
if (!pageFiles.length) {
|
|
159
221
|
console.log('kmwc check-pages: no example pages — nothing to check.');
|
|
160
222
|
return 0;
|
|
@@ -172,10 +234,13 @@ export async function run(args) {
|
|
|
172
234
|
let problemCount = 0;
|
|
173
235
|
try {
|
|
174
236
|
console.log(`kmwc check-pages — ${pageFiles.length} page(s) × ${widths.join(' / ')}px on ${baseUrl}\n`);
|
|
175
|
-
for (const pageFile of pageFiles) {
|
|
237
|
+
for (const [pageIndex, pageFile] of pageFiles.entries()) {
|
|
176
238
|
const pageProblems = [];
|
|
177
|
-
|
|
178
|
-
|
|
239
|
+
const pageRules = loadPageRules(config.root, pagePaths[pageIndex]);
|
|
240
|
+
for (const [widthIndex, width] of widths.entries()) {
|
|
241
|
+
// Shadow-only rules don't depend on the width: check them on the first load only.
|
|
242
|
+
const report = await loadPageReport(browser, `${baseUrl}/${pageFile}`, width, widthIndex === 0 ? pageRules : []);
|
|
243
|
+
for (const problem of getPageProblems(report)) pageProblems.push(`@${width}px ${problem}`);
|
|
179
244
|
}
|
|
180
245
|
problemCount += pageProblems.length;
|
|
181
246
|
console.log(`${pageProblems.length ? '✗' : '✓'} ${pageFile}${pageProblems.map((problem) => `\n ${problem}`).join('')}`);
|
|
@@ -213,13 +213,13 @@
|
|
|
213
213
|
|
|
214
214
|
button.secondary,
|
|
215
215
|
button.btn-secondary {
|
|
216
|
-
background: var(--kmwc-neutral-color);
|
|
216
|
+
background: var(--kmwc-neutral-fill-color);
|
|
217
217
|
color: var(--kmwc-text-color-on-neutral);
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
button.secondary:hover,
|
|
221
221
|
button.btn-secondary:hover {
|
|
222
|
-
background:
|
|
222
|
+
background: var(--kmwc-neutral-fill-color-hover);
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
button.btn-outline {
|
|
@@ -137,10 +137,15 @@
|
|
|
137
137
|
--kmwc-danger-fill-color-hover: var(--base-danger-color-hover, light-dark(color-mix(in srgb, black 15%, var(--kmwc-danger-fill-color)), color-mix(in srgb, white 15%, var(--kmwc-danger-fill-color))));
|
|
138
138
|
--kmwc-text-color-on-danger: var(--base-on-danger-text-color, color(from color(from var(--kmwc-danger-fill-color) srgb calc(0.21 * r * r * (0.4 + r * (0.82 - 0.22 * r)) + 0.72 * g * g * (0.4 + g * (0.82 - 0.22 * g)) + 0.07 * b * b * (0.4 + b * (0.82 - 0.22 * b))) 0 0) srgb clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1)));
|
|
139
139
|
--kmwc-neutral-color: light-dark(#6b7280, #9ca3af);
|
|
140
|
+
/* A grey button needs its own fill: the dark-mode neutral (#9ca3af) sits just under the
|
|
141
|
+
on-colour formula's 0.34 switch, so it got white text at 2.5:1. Dots and pills keep the
|
|
142
|
+
light grey; the button takes a dark one (white text ≥ 4.5:1 in both themes). */
|
|
143
|
+
--kmwc-neutral-fill-color: light-dark(#6b7280, #4b5563);
|
|
144
|
+
--kmwc-neutral-fill-color-hover: light-dark(color-mix(in srgb, black 15%, var(--kmwc-neutral-fill-color)), color-mix(in srgb, white 12%, var(--kmwc-neutral-fill-color)));
|
|
140
145
|
--kmwc-badge-new-bg: light-dark(#e53e3e, #f56565);
|
|
141
146
|
/* Text on the neutral / badge fills: the contract's on-colour formula on
|
|
142
147
|
their own background, not on the accent (a light primary would turn it black). */
|
|
143
|
-
--kmwc-text-color-on-neutral: color(from color(from var(--kmwc-neutral-color) srgb calc(0.21 * r * r * (0.4 + r * (0.82 - 0.22 * r)) + 0.72 * g * g * (0.4 + g * (0.82 - 0.22 * g)) + 0.07 * b * b * (0.4 + b * (0.82 - 0.22 * b))) 0 0) srgb clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1));
|
|
148
|
+
--kmwc-text-color-on-neutral: color(from color(from var(--kmwc-neutral-fill-color) srgb calc(0.21 * r * r * (0.4 + r * (0.82 - 0.22 * r)) + 0.72 * g * g * (0.4 + g * (0.82 - 0.22 * g)) + 0.07 * b * b * (0.4 + b * (0.82 - 0.22 * b))) 0 0) srgb clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1));
|
|
144
149
|
--kmwc-text-color-on-badge-new: color(from color(from var(--kmwc-badge-new-bg) srgb calc(0.21 * r * r * (0.4 + r * (0.82 - 0.22 * r)) + 0.72 * g * g * (0.4 + g * (0.82 - 0.22 * g)) + 0.07 * b * b * (0.4 + b * (0.82 - 0.22 * b))) 0 0) srgb clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1) clamp(0, (0.34 - r) * 1e9, 1));
|
|
145
150
|
|
|
146
151
|
/* ======================================================================
|
|
@@ -54,4 +54,6 @@ locally — every local fork is how they drifted apart.
|
|
|
54
54
|
`*.checks.sh` suites. A rulebook failure that is a real architectural decision
|
|
55
55
|
goes into `VALIDATION-NOTES.md` under a `## C-XX-N — …` heading (deferrals
|
|
56
56
|
don't count). After changing a page, `npx kmwc check-pages` loads every page at
|
|
57
|
-
desktop and phone widths and reports errors and
|
|
57
|
+
desktop and phone widths and reports errors, horizontal overflow and page CSS
|
|
58
|
+
that only matches inside a shadow root (markup a render callback draws there is
|
|
59
|
+
out of page CSS's reach — style it through the component's styles hook).
|