diff --git a/scripts/check-duplication.mjs b/scripts/check-duplication.mjs index 76fae8cf..217c9064 100644 --- a/scripts/check-duplication.mjs +++ b/scripts/check-duplication.mjs @@ -36,7 +36,7 @@ export const repoRoot = dirname(dirname(fileURLToPath(import.meta.url))); // them. A change that genuinely needs a new copy should instead extract the // shared piece, which is what the count exists to prompt. export const CLONE_CEILING = 245; -export const CSS_YAML_CLONE_CEILING = 35; +export const CSS_YAML_CLONE_CEILING = 30; // How far below the ceiling the count may fall before the gate insists the // ceiling follow it down. Without this the slack left by one clean-up would diff --git a/src/components/CNCFProjectCard/styles.module.css b/src/components/CNCFProjectCard/styles.module.css index 2666a3c4..93dc4888 100644 --- a/src/components/CNCFProjectCard/styles.module.css +++ b/src/components/CNCFProjectCard/styles.module.css @@ -1,4 +1,6 @@ .card { + composes: cardLift from '../shared/InteractiveCard.module.css'; + --card-lift-shadow: 0 4px 12px rgb(0 0 0 / 8%); align-items: center; aspect-ratio: 1 / 1; background: var(--ifm-background-surface-color); @@ -23,19 +25,6 @@ width: 120px; } -.card:hover, -.card:focus-visible { - border-color: var(--ifm-color-primary); - box-shadow: 0 4px 12px rgb(0 0 0 / 8%); - color: inherit; - transform: translateY(-2px); -} - -.card:focus-visible { - outline: 3px solid var(--ifm-color-primary-light); - outline-offset: 2px; -} - .header { align-items: center; display: flex; diff --git a/src/components/MetricsDashboard/styles.module.css b/src/components/MetricsDashboard/styles.module.css index 84443bfa..1d8d79d3 100644 --- a/src/components/MetricsDashboard/styles.module.css +++ b/src/components/MetricsDashboard/styles.module.css @@ -9,6 +9,7 @@ gap: 1rem; } .card { + composes: cardLift from '../shared/InteractiveCard.module.css'; border: 1px solid var(--ifm-color-emphasis-200); color: inherit; display: flex; @@ -21,17 +22,6 @@ transform 160ms ease, box-shadow 160ms ease; } -.card:hover, -.card:focus-visible { - border-color: var(--ifm-color-primary); - box-shadow: 0 8px 24px rgb(0 0 0 / 8%); - color: inherit; - transform: translateY(-2px); -} -.card:focus-visible { - outline: 3px solid var(--ifm-color-primary-light); - outline-offset: 2px; -} .label { font-weight: 700; } diff --git a/src/components/ReferenceArchitectures/styles.module.css b/src/components/ReferenceArchitectures/styles.module.css index 83552308..d108dfa1 100644 --- a/src/components/ReferenceArchitectures/styles.module.css +++ b/src/components/ReferenceArchitectures/styles.module.css @@ -12,6 +12,9 @@ } .card { + composes: cardLift from '../shared/InteractiveCard.module.css'; + --card-lift-shadow: 0 8px 24px rgb(0 0 0 / 10%); + --card-lift-outline-offset: 3px; display: flex; min-width: 0; flex-direction: column; @@ -26,19 +29,6 @@ transform 160ms ease; } -.card:hover, -.card:focus-visible { - border-color: var(--ifm-color-primary); - box-shadow: 0 8px 24px rgb(0 0 0 / 10%); - color: inherit; - transform: translateY(-2px); -} - -.card:focus-visible { - outline: 3px solid var(--ifm-color-primary-light); - outline-offset: 3px; -} - .cardContent { display: flex; flex: 1; diff --git a/src/components/shared/InteractiveCard.module.css b/src/components/shared/InteractiveCard.module.css new file mode 100644 index 00000000..bec7c16b --- /dev/null +++ b/src/components/shared/InteractiveCard.module.css @@ -0,0 +1,21 @@ +/* + * Shared hover/focus-lift interaction for clickable "card" components + * (CNCFProjectCard, MetricsDashboard, ReferenceArchitectures). Each consumer's + * `.card` carries its own box geometry (sizing, layout, border-radius), but + * all three repeat the same lift-on-hover/focus treatment: border-color and + * transform on hover/focus-visible, plus a focus-visible outline. Shadow + * depth and outline-offset vary slightly by consumer, so `.cardLift` reads + * them from custom properties set by the consumer's own `.card` rule. + */ +.cardLift:hover, +.cardLift:focus-visible { + border-color: var(--ifm-color-primary); + box-shadow: var(--card-lift-shadow, 0 8px 24px rgb(0 0 0 / 8%)); + color: inherit; + transform: translateY(-2px); +} + +.cardLift:focus-visible { + outline: 3px solid var(--ifm-color-primary-light); + outline-offset: var(--card-lift-outline-offset, 2px); +} diff --git a/tests/css-modules.test.mjs b/tests/css-modules.test.mjs index 0e387fe3..7e5b6f73 100644 --- a/tests/css-modules.test.mjs +++ b/tests/css-modules.test.mjs @@ -152,13 +152,24 @@ test('no CSS Module class is declared without a reader', () => { if (hasComputedLookup(source, binding)) unresolvable.add(stylesheet); } + // A class reached only through `composes` has no JS reader by design. + // `composes: foo;` (no `from`) reaches a class declared in this same + // stylesheet; `composes: foo from './x.css'` reaches one declared in the + // target stylesheet instead, so the "seen" mark belongs there, not here. + for (const stylesheet of cssModules) { + const css = readFileSync(stylesheet, 'utf8'); + for (const { name, from } of composedNamesIn(css)) { + const target = from ? resolve(dirname(stylesheet), from) : stylesheet; + if (!referenced.has(target)) referenced.set(target, new Set()); + referenced.get(target).add(name); + } + } + const orphans = []; for (const stylesheet of cssModules) { if (unresolvable.has(stylesheet)) continue; const css = readFileSync(stylesheet, 'utf8'); const seen = referenced.get(stylesheet) ?? new Set(); - // A class reached only through `composes` has no JS reader by design. - for (const { name } of composedNamesIn(css)) seen.add(name); for (const name of declaredClassesIn(css)) { if (seen.has(name)) continue; if (KNOWN_ORPHANS.has(key(stylesheet, name))) continue;