diff --git a/apps/web/src/components/bildirim/BildirimPopover.css b/apps/web/src/components/bildirim/BildirimPopover.css index 2c4e928bd..043a128c5 100644 --- a/apps/web/src/components/bildirim/BildirimPopover.css +++ b/apps/web/src/components/bildirim/BildirimPopover.css @@ -30,7 +30,6 @@ } .kp-bildirim-pop__count { font: var(--t-meta); - color: var(--text-primary); } /* The Positioner carries the stacking rank (the `position: fixed` element), the diff --git a/apps/web/src/components/divan/CaylakDetail.tsx b/apps/web/src/components/divan/CaylakDetail.tsx index 0956280ef..f954173d9 100644 --- a/apps/web/src/components/divan/CaylakDetail.tsx +++ b/apps/web/src/components/divan/CaylakDetail.tsx @@ -29,9 +29,9 @@ import type {Tier} from "../../../worker/features/kunye/standing"; import {Screen} from "../../fate/Screen"; import {codeOf} from "../../fate/wire"; import {Alert} from "../ui/Alert"; -import {Badge} from "../ui/Badge"; import {Button} from "../ui/Button"; import {ReportButton, type ReportOutcome} from "../ui/ReportButton"; +import {ReviewBadge} from "../ui/ReviewBadge"; import {VoteTriangle} from "../VoteTriangle"; import {CaylakIdentityById, IdentityFallback} from "./CaylakIdentity"; import { @@ -278,9 +278,7 @@ function BacklogItemRow({node}: {readonly node: ViewRef<"DivanBacklogItem">}) {
{itemKindLabel(data.kind)} - - incelemede - +

{data.preview || "(boş)"}

diff --git a/apps/web/src/components/divan/Divan.css b/apps/web/src/components/divan/Divan.css index 62595a54a..96626a88c 100644 --- a/apps/web/src/components/divan/Divan.css +++ b/apps/web/src/components/divan/Divan.css @@ -385,12 +385,13 @@ text-transform: lowercase; } +/* Geometry only — the variant owns the colours. Manti paints a variant as a PAIR + (`background: var(--variant-solid); color: var(--variant-on-solid)`), so a class that + overrode half of it left the other half's fill showing underneath (#5228). */ .kp-divan__badge { font: var(--t-meta); padding: 1px 6px; border-radius: var(--r-pill); - border: 1px solid var(--border); - color: var(--text-muted); } .kp-divan__preview { @@ -498,8 +499,6 @@ font: var(--t-meta); padding: 1px 6px; border-radius: var(--r-pill); - border: 1px solid var(--border); - color: var(--text-muted); } .kp-triage__age { diff --git a/apps/web/src/components/profile/ContributionRow.css b/apps/web/src/components/profile/ContributionRow.css index 486439f1d..7c6b8d6aa 100644 --- a/apps/web/src/components/profile/ContributionRow.css +++ b/apps/web/src/components/profile/ContributionRow.css @@ -42,18 +42,6 @@ color: var(--tag-ask-fg); } -/* "incelemede" badge on the owner's own still-sandboxed item (#1291). State is - carried by the word, not color alone; the role tokens are AA-contrast. */ -.kp-user-profile__badge { - font: var(--t-meta); - font-weight: 600; - padding: 2px 6px; - border-radius: 4px; - background: var(--warning-soft, var(--accent-soft)); - color: var(--warning, var(--accent)); - text-transform: lowercase; -} - .kp-user-profile__row-title { color: var(--text-primary); text-decoration: none; diff --git a/apps/web/src/components/profile/ContributionRow.tsx b/apps/web/src/components/profile/ContributionRow.tsx index 56077b80c..7b63f5611 100644 --- a/apps/web/src/components/profile/ContributionRow.tsx +++ b/apps/web/src/components/profile/ContributionRow.tsx @@ -8,6 +8,7 @@ import {toIso} from "../../fate/wire"; import {formatAgoTR} from "../../lib/datetime"; import {renderMarkdownInline} from "../../lib/markdown"; import {Badge} from "../ui/Badge"; +import {ReviewBadge} from "../ui/ReviewBadge"; import "./ContributionRow.css"; export const ContributionView = view()({ @@ -41,12 +42,7 @@ export interface ContributionRowProps { export function ContributionRow({node, sandboxBadge = false}: ContributionRowProps) { const c = useView(ContributionView, node); - const badge = - sandboxBadge && c.sandboxed ? ( - - incelemede - - ) : null; + const badge = sandboxBadge && c.sandboxed ? : null; if (c.kind === "definition") { return ( diff --git a/apps/web/src/components/ui/ReviewBadge.tsx b/apps/web/src/components/ui/ReviewBadge.tsx index d2e4a1bf5..97b9d975c 100644 --- a/apps/web/src/components/ui/ReviewBadge.tsx +++ b/apps/web/src/components/ui/ReviewBadge.tsx @@ -1,17 +1,17 @@ -// The "incelemede" in-review badge a çaylak sees on their OWN sandboxed content -// (#2200). The `sandboxed` wire flag is owner-scoped server-side, so a caller only -// ever has it `true` for the author themselves — this is the shared render for the -// post-detail + definition surfaces (the profile katkıların list has its own #1291 -// badge). State is carried by the word, not color alone (the AA-contrast tokens). +// The "incelemede" in-review badge (#2200) — the ONE renderer of that state, on every +// surface that shows it: the author's own sandboxed content (post-detail, definition, +// the profile katkıların list) and the divan's review queue. The profile list and divan +// each carried their own copy until #5228 folded them in. State is carried by the word, +// not color alone (the AA-contrast tokens). import {Badge} from "./Badge"; import "./ReviewBadge.css"; /** * @component ReviewBadge - * @whenToUse The incelemede in-review badge. Reach for it on a çaylak's OWN - * sandboxed content (post-detail, definition) to signal it is pending review — - * the `sandboxed` wire flag is owner-scoped, so only the author sees it. The - * profile katkıların list has its own #1291 badge; don't reuse this there. + * @whenToUse Every "incelemede" (pending review) badge, wherever it renders — the + * author's own sandboxed content (post-detail, definition, profile katkıların) and + * the divan review queue. Never hand-roll a second one; a feature class over a raw + * `Badge` variant is how the divan chip ended up grey-on-blue (#5228). * @slot none Fixed copy; no children slot. */ export function ReviewBadge() { diff --git a/apps/web/src/components/ui/badge-variant-contract.unit.test.ts b/apps/web/src/components/ui/badge-variant-contract.unit.test.ts new file mode 100644 index 000000000..f9bbd3bac --- /dev/null +++ b/apps/web/src/components/ui/badge-variant-contract.unit.test.ts @@ -0,0 +1,98 @@ +// Manti paints a badge variant as a PAIR — `background: var(--variant-solid); color: +// var(--variant-on-solid)` from the manti.components layer — so a feature class that overrides +// one half leaves the other half's fill underneath. That is how the divan's "incelemede" chip +// rendered muted-grey text on the raw `info` blue (#5228). +import {readdirSync, readFileSync} from "node:fs"; +import {join, relative} from "node:path"; +import {describe, expect, it} from "vitest"; + +const sourceRoot = join(import.meta.dirname, "../.."); +const badgeElementPattern = /]*>/gs; +const classNamePattern = /className=(?:"([^"]*)"|\{`([^`]*)`\})/; + +function sourceFiles(directory: string, extension: string): string[] { + return readdirSync(directory, {withFileTypes: true}).flatMap((entry) => { + const path = join(directory, entry.name); + + if (entry.isDirectory()) return sourceFiles(path, extension); + if (!entry.name.endsWith(extension) || entry.name.includes(".test.")) return []; + + return [path]; + }); +} + +/** Every static `kp-*` class a production `` carries. */ +function variantBadgeClasses(): Set { + const classes = new Set(); + + for (const path of sourceFiles(sourceRoot, ".tsx")) { + for (const element of readFileSync(path, "utf8").matchAll(badgeElementPattern)) { + if (!/\bvariant=/.test(element[0])) continue; + + const match = classNamePattern.exec(element[0]); + const value = match?.[1] ?? match?.[2]; + if (!value) continue; + + for (const name of value.split(/\s+/)) { + if (name.startsWith("kp-")) classes.add(name); + } + } + } + + return classes; +} + +type Rule = {file: string; selector: string; body: string}; + +function cssRules(): Rule[] { + return sourceFiles(sourceRoot, ".css").flatMap((path) => + [...readFileSync(path, "utf8").matchAll(/([^{}]+)\{([^{}]*)\}/g)].map((rule) => ({ + file: relative(sourceRoot, path), + selector: (rule[1] ?? "").replace(/\/\*[\s\S]*?\*\//g, "").trim(), + body: rule[2] ?? "", + })), + ); +} + +function declares(body: string, property: RegExp): boolean { + return property.test(body); +} + +/** The rule targets the badge itself only when the class sits in the selector's last compound. */ +function targets(selector: string, className: string): boolean { + return selector + .split(",") + .some((part) => part.trim().split(/\s+/).at(-1)?.includes(`.${className}`) === true); +} + +describe("Manti badge variant contract", () => { + const classes = variantBadgeClasses(); + const rules = cssRules(); + + it("scans a non-empty set of variant badge classes and CSS rules", () => { + expect(classes.size).toBeGreaterThan(0); + expect(rules.length).toBeGreaterThan(0); + }); + + it("overrides a variant's colour pair whole, never half of it", () => { + const offenders = rules + .filter((rule) => [...classes].some((name) => targets(rule.selector, name))) + .filter((rule) => { + const color = declares(rule.body, /(^|[;{\s])color\s*:/); + const background = declares(rule.body, /(^|[;{\s])background(-color)?\s*:/); + return color !== background; + }) + .map((rule) => `${rule.file} :: ${rule.selector}`); + + expect(offenders).toEqual([]); + }); + + it("renders the incelemede state through ReviewBadge alone", () => { + const offenders = sourceFiles(sourceRoot, ".tsx") + .filter((path) => !path.endsWith("ReviewBadge.tsx")) + .filter((path) => / relative(sourceRoot, path)); + + expect(offenders).toEqual([]); + }); +}); diff --git a/design-system-inventory.md b/design-system-inventory.md index b7f762c4f..08f88d1d9 100644 --- a/design-system-inventory.md +++ b/design-system-inventory.md @@ -224,7 +224,7 @@ _Source: apps/web/src/components/ui/ReportButton.tsx_ _Source: apps/web/src/components/ui/ReviewBadge.tsx_ -**When to use:** The incelemede in-review badge. Reach for it on a çaylak's OWN sandboxed content (post-detail, definition) to signal it is pending review — the `sandboxed` wire flag is owner-scoped, so only the author sees it. The profile katkıların list has its own #1291 badge; don't reuse this there. +**When to use:** Every "incelemede" (pending review) badge, wherever it renders — the author's own sandboxed content (post-detail, definition, profile katkıların) and the divan review queue. Never hand-roll a second one; a feature class over a raw `Badge` variant is how the divan chip ended up grey-on-blue (#5228). **Slots:** - `none` — Fixed copy; no children slot.