Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion apps/web/src/components/bildirim/BildirimPopover.css
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions apps/web/src/components/divan/CaylakDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -278,9 +278,7 @@ function BacklogItemRow({node}: {readonly node: ViewRef<"DivanBacklogItem">}) {
<div className="kp-divan__item-body">
<div className="kp-divan__item-meta">
<span className="kp-divan__kind">{itemKindLabel(data.kind)}</span>
<Badge variant="info" className="kp-divan__badge" data-testid="incelemede-badge">
incelemede
</Badge>
<ReviewBadge />
</div>
<p className="kp-divan__preview">{data.preview || "(boş)"}</p>
</div>
Expand Down
7 changes: 3 additions & 4 deletions apps/web/src/components/divan/Divan.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -746,7 +745,7 @@
color: var(--danger);
}

.kp-wave__check {

Check warning on line 748 in apps/web/src/components/divan/Divan.css

View workflow job for this annotation

GitHub Actions / lint / format / typecheck

lint/style/noDescendingSpecificity

Descending specificity selector found. This selector specificity is (0, 1, 0)
color: var(--text-muted);
}

Expand Down
12 changes: 0 additions & 12 deletions apps/web/src/components/profile/ContributionRow.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 2 additions & 6 deletions apps/web/src/components/profile/ContributionRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Contribution>()({
Expand Down Expand Up @@ -41,12 +42,7 @@ export interface ContributionRowProps {

export function ContributionRow({node, sandboxBadge = false}: ContributionRowProps) {
const c = useView(ContributionView, node);
const badge =
sandboxBadge && c.sandboxed ? (
<Badge variant="info" className="kp-user-profile__badge" data-testid="incelemede-badge">
incelemede
</Badge>
) : null;
const badge = sandboxBadge && c.sandboxed ? <ReviewBadge /> : null;

if (c.kind === "definition") {
return (
Expand Down
18 changes: 9 additions & 9 deletions apps/web/src/components/ui/ReviewBadge.tsx
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down
98 changes: 98 additions & 0 deletions apps/web/src/components/ui/badge-variant-contract.unit.test.ts
Original file line number Diff line number Diff line change
@@ -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 = /<Badge\b[^>]*>/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 `<Badge variant="…">` carries. */
function variantBadgeClasses(): Set<string> {
const classes = new Set<string>();

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) => /<Badge\b[\s\S]*?incelemede/.test(readFileSync(path, "utf8")))
.map((path) => relative(sourceRoot, path));

expect(offenders).toEqual([]);
});
});
2 changes: 1 addition & 1 deletion design-system-inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading