chain line: the logic is the connective — LaTeX ∧ and ⟹ via KaTeX (owner ruling)
'A → B → C' misread as chained implication when A and B are CO-premises. The honest shape (owner, 2026-09-30): premises join with the LaTeX LAND (∧), the conclusion separates by the LaTeX IMPLICATION (⟹) — A ∧ B ⟹ C. The glyphs are REAL KaTeX renders — components/latex-glyph.tsx wraps katex.renderToString (cached per command; the app's math engine, the stylesheet already rides app-wide from app/layout.tsx). No SVGs, no font-dependent unicode. PROMINENT in the theme foreground (white in the dark theme) per the owner's same-round ruling — the logic is the payload, not the old dim gray. A lonely chip carries no glyph (it reads as a standalone proposition); the plain-text '→' survives only in the row's aria-label (formatChainLine, the screen-reader form).
This commit is contained in:
+44
-32
@@ -21,26 +21,15 @@
|
|||||||
* (vertical column hairlines removed with them, owner ruling: "anything
|
* (vertical column hairlines removed with them, owner ruling: "anything
|
||||||
* else is better without them").
|
* else is better without them").
|
||||||
*
|
*
|
||||||
* THE CHAIN LINE: every proposition name is a faint-outlined CHIP with
|
* THE CHAIN LINE: every proposition name is a faint-outlined CHIP
|
||||||
* the → a clearly separated connector between chips — a lonely chip
|
* with the LOGIC as the connectives (owner rulings, 2026-09-30):
|
||||||
* reads as a standalone proposition (the "(no premises)" annotation is
|
* premises join with the LaTeX LAND (∧) and the conclusion separates
|
||||||
* dead in the visible UI; it survives only as the row's aria-label).
|
* by the LaTeX IMPLICATION (⟹) — `A ∧ B ⟹ C`, never the misleading
|
||||||
* Icons: ONLY the cluster FolderIcon survives (owner ruling, evening
|
* `A → B → C`. The glyphs are REAL KaTeX renders (components/
|
||||||
* round 3: the chain waypoints icons are not needed — the chips +
|
* latex-glyph.tsx — the app's math engine, never SVGs), PROMINENT in
|
||||||
* connectors already read as chains); propositions chip-only. Chevrons
|
* the theme foreground (white), not dim gray. A lonely chip (no
|
||||||
* rotate with expansion. Statement rows show propositions PLAIN (owner
|
* premises) carries no glyph — it reads as a standalone proposition;
|
||||||
* ruling: the derivation is already encoded in the chain line — no
|
* "(no premises)" survives only as the row's aria-label.
|
||||||
* conclusion marker down there).
|
|
||||||
*
|
|
||||||
* THE ALIGNMENT STAIRCASE (owner ruling, round 3): every level's
|
|
||||||
* checkbox sits ONE COLUMN under its parent's expander — aligned with
|
|
||||||
* the parent's ICON slot: open a folder and the chain checkboxes line
|
|
||||||
* up with the FOLDER icon; the statements step one more unit out (the
|
|
||||||
* would-be chain-icon column — the chains lost their icons but the
|
|
||||||
* pattern continues, owner: "double the indentation"). Geometry: every
|
|
||||||
* column is 14px + 4px gap = 18px and each step is TWO columns (36px) —
|
|
||||||
* clusters pl-3 (12px), chains pl-12 (48px = the cluster's folder
|
|
||||||
* icon), statements pl-21 (84px = where a chain icon WOULD sit).
|
|
||||||
*
|
*
|
||||||
* SORTING: all three columns sortable (owner ruling — "consistency is
|
* SORTING: all three columns sortable (owner ruling — "consistency is
|
||||||
* key"): Name (A→Z), Props (largest-first), Created (newest-first);
|
* key"): Name (A→Z), Props (largest-first), Created (newest-first);
|
||||||
@@ -83,6 +72,7 @@ import type { GraphEdge, GraphNode } from "@/src/core";
|
|||||||
import { TopBar } from "@/components/top-bar";
|
import { TopBar } from "@/components/top-bar";
|
||||||
import { PracticeDialog } from "@/components/practice-dialog";
|
import { PracticeDialog } from "@/components/practice-dialog";
|
||||||
import { DeleteNodeDialog } from "@/components/delete-node-dialog";
|
import { DeleteNodeDialog } from "@/components/delete-node-dialog";
|
||||||
|
import { LatexGlyph } from "@/components/latex-glyph";
|
||||||
|
|
||||||
/** The sortable columns (owner ruling: all three — consistency). */
|
/** The sortable columns (owner ruling: all three — consistency). */
|
||||||
export type ChainSortCol = "name" | "props" | "created";
|
export type ChainSortCol = "name" | "props" | "created";
|
||||||
@@ -405,20 +395,42 @@ export function ChainTableBody({
|
|||||||
/>
|
/>
|
||||||
<Expander open={chainOpen} onClick={() => onToggleChain(chain.id)} testid={`chain-toggle-${chain.id}`} label={`Expand chain ${nameOf(chain.conclusion)}`} />
|
<Expander open={chainOpen} onClick={() => onToggleChain(chain.id)} testid={`chain-toggle-${chain.id}`} label={`Expand chain ${nameOf(chain.conclusion)}`} />
|
||||||
{/* The chain line: proposition names as faint-outlined
|
{/* The chain line: proposition names as faint-outlined
|
||||||
chips, → the connector BETWEEN them (owner ruling) — a
|
chips. THE LOGIC IS THE PAYLOAD (owner rulings,
|
||||||
lonely chip reads as a standalone proposition. */}
|
2026-09-30): premises join with the LaTeX LAND (∧),
|
||||||
|
the conclusion separates by the LaTeX IMPLICATION
|
||||||
|
(⟹) — `A ∧ B ⟹ C`, never the misleading `A → B →
|
||||||
|
C` — the glyphs render through KaTeX (the app's
|
||||||
|
math engine), PROMINENT in the theme foreground
|
||||||
|
(white), not the old dim gray. A lonely chip reads
|
||||||
|
as a standalone proposition (no glyphs at all);
|
||||||
|
the row's aria-label keeps the plain-text form. */}
|
||||||
<span className="flex min-w-0 flex-1 flex-wrap items-center gap-x-0.5 gap-y-1 text-[13px] leading-tight">
|
<span className="flex min-w-0 flex-1 flex-wrap items-center gap-x-0.5 gap-y-1 text-[13px] leading-tight">
|
||||||
{memberIds.map((id, i) => (
|
{memberIds.map((id, i) => {
|
||||||
<span key={id} className="flex items-center">
|
const isConclusion = i === memberIds.length - 1;
|
||||||
{i > 0 && <span data-testid={`chain-arrow-${chain.id}-${i}`} className="px-1.5 text-muted-foreground/50">→</span>}
|
return (
|
||||||
<span
|
<span key={id} className="flex items-center">
|
||||||
data-testid={`prop-chip-${id}`}
|
{i > 0 && (isConclusion ? (
|
||||||
className="rounded border border-border/70 bg-muted/30 px-1.5 py-0.5"
|
<LatexGlyph
|
||||||
>
|
cmd="\implies"
|
||||||
{nameOf(id)}
|
testid={`chain-implies-${chain.id}`}
|
||||||
|
className="px-1.5 text-foreground"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LatexGlyph
|
||||||
|
cmd="\land"
|
||||||
|
testid={`chain-and-${chain.id}-${i}`}
|
||||||
|
className="px-1.5 text-foreground"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<span
|
||||||
|
data-testid={`prop-chip-${id}`}
|
||||||
|
className="rounded border border-border/70 bg-muted/30 px-1.5 py-0.5"
|
||||||
|
>
|
||||||
|
{nameOf(id)}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
);
|
||||||
))}
|
})}
|
||||||
</span>
|
</span>
|
||||||
<span className="w-14 shrink-0 text-right text-xs tabular-nums text-muted-foreground">
|
<span className="w-14 shrink-0 text-right text-xs tabular-nums text-muted-foreground">
|
||||||
{memberIds.length}
|
{memberIds.length}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import katex from "katex";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* THE LaTeX glyph seam (owner ruling, 2026-09-30): UI connectors that
|
||||||
|
* want math symbols render the REAL LaTeX glyphs through KaTeX — the
|
||||||
|
* app's math engine, already shipped (app/layout.tsx carries the
|
||||||
|
* katex stylesheet) — never hand-rolled SVGs or font-dependent
|
||||||
|
* unicode. Born for the chain table's logical connectives: premises
|
||||||
|
* join with \land (∧), the conclusion separates with \implies (⟹) —
|
||||||
|
* `A ∧ B ⟹ C`, not the misleading `A → B → C`.
|
||||||
|
*
|
||||||
|
* Prominent by the same ruling: the connectives render in the
|
||||||
|
* theme's FOREGROUND (white in the dark theme) — the chain line's
|
||||||
|
* logic is the payload, not decoration. Callers pass the class.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Cached katex HTML per command — the glyph set is tiny and
|
||||||
|
* renderToString is pure; one render per command per process. */
|
||||||
|
const cache = new Map<string, string>();
|
||||||
|
|
||||||
|
/** The raw katex HTML for a LaTeX command (pure, sync — SSR-safe). */
|
||||||
|
export function latexGlyphHtml(cmd: string): string {
|
||||||
|
let html = cache.get(cmd);
|
||||||
|
if (html === undefined) {
|
||||||
|
html = katex.renderToString(cmd, { throwOnError: false, output: "html" });
|
||||||
|
cache.set(cmd, html);
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One rendered LaTeX glyph. `cmd` is the LaTeX command ("\land",
|
||||||
|
* "\implies"); the output is aria-hidden — callers carry the textual
|
||||||
|
* aria-label (formatChainLine keeps the plain-text form). */
|
||||||
|
export function LatexGlyph({
|
||||||
|
cmd,
|
||||||
|
testid,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
cmd: string;
|
||||||
|
testid?: string;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-testid={testid}
|
||||||
|
aria-hidden="true"
|
||||||
|
className={className}
|
||||||
|
// katex.renderToString is the library's own trusted output
|
||||||
|
// (the same thing rehype-katex injects for markdown math).
|
||||||
|
dangerouslySetInnerHTML={{ __html: latexGlyphHtml(cmd) }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
@@ -5,7 +5,9 @@
|
|||||||
* surface), cluster → chain → statement hierarchy (a chain lives in its
|
* surface), cluster → chain → statement hierarchy (a chain lives in its
|
||||||
* CONCLUSION's birth cluster; expanding a chain reveals statements
|
* CONCLUSION's birth cluster; expanding a chain reveals statements
|
||||||
* directly — names live in the line, never twice), proposition CHIPS +
|
* directly — names live in the line, never twice), proposition CHIPS +
|
||||||
* → connectors (a lonely chip = a standalone proposition; the "(no
|
* ∧/⟹ LaTeX connectives (premises join by land, the conclusion
|
||||||
|
* separates by implication — REAL KaTeX renders, prominent white; a
|
||||||
|
* lonely chip = a standalone proposition; the "(no
|
||||||
* premises)" annotation is dead), rotating chevrons, folder/waypoints
|
* premises)" annotation is dead), rotating chevrons, folder/waypoints
|
||||||
* icons, a floating bottom selection bar that never shifts layout, and
|
* icons, a floating bottom selection bar that never shifts layout, and
|
||||||
* a fuzzy search that auto-expands matching clusters.
|
* a fuzzy search that auto-expands matching clusters.
|
||||||
@@ -164,14 +166,22 @@ describe("ChainTableBody — THE shared TopBar + the lines law (owner, round 2)"
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("ChainTableBody — chain rows (chips + connectors)", () => {
|
describe("ChainTableBody — chain rows (chips + connectors)", () => {
|
||||||
it("names render as outlined chips with → connectors BETWEEN them", () => {
|
it("names render as outlined chips; premises join by ∧, the conclusion by ⟹ — REAL KaTeX glyphs in the foreground", () => {
|
||||||
const html = render({ expandedClusters: new Set(["chat1"]) });
|
const html = render({ expandedClusters: new Set(["chat1"]) });
|
||||||
assert.ok(html.includes('data-testid="chain-row-c1"'));
|
assert.ok(html.includes('data-testid="chain-row-c1"'));
|
||||||
assert.ok(html.includes('data-testid="prop-chip-a"'), "premise chip");
|
assert.ok(html.includes('data-testid="prop-chip-a"'), "premise chip");
|
||||||
assert.ok(html.includes('data-testid="prop-chip-c1"'), "conclusion chip");
|
assert.ok(html.includes('data-testid="prop-chip-c1"'), "conclusion chip");
|
||||||
assert.ok(html.includes("Premise one"), "chip text");
|
assert.ok(html.includes("Premise one"), "chip text");
|
||||||
assert.ok(html.includes('data-testid="chain-arrow-c1-1"'), "the connector");
|
// the connectives (owner ruling, 2026-09-30): premises ∧, conclusion
|
||||||
assert.ok(html.includes("→"), "the arrow glyph");
|
// ⟹ — KaTeX renders (class katex present), PROMINENT foreground
|
||||||
|
assert.ok(html.includes('data-testid="chain-and-c1-1"'), "the land between premises");
|
||||||
|
assert.ok(html.includes('data-testid="chain-implies-c1"'), "the implication before the conclusion");
|
||||||
|
const and = html.match(/data-testid="chain-and-c1-1"[^>]*>/)?.[0] ?? "";
|
||||||
|
assert.match(and, /text-foreground/, "prominent — the theme's white, not dim gray");
|
||||||
|
assert.ok(html.includes("katex"), "a REAL KaTeX render, not a unicode arrow");
|
||||||
|
// the old arrow is dead as a VISIBLE connector — it survives only in
|
||||||
|
// the row's aria-label (formatChainLine, the plain-text form)
|
||||||
|
assert.doesNotMatch(html, />\s*→\s*</, "no textual arrow connector remains");
|
||||||
// owner ruling (round 3): the chain waypoints icons are GONE — the
|
// owner ruling (round 3): the chain waypoints icons are GONE — the
|
||||||
// chips + connectors already read as chains; only folders keep icons
|
// chips + connectors already read as chains; only folders keep icons
|
||||||
assert.ok(!html.includes("lucide-waypoints"), "no chain icon");
|
assert.ok(!html.includes("lucide-waypoints"), "no chain icon");
|
||||||
|
|||||||
Reference in New Issue
Block a user