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.