Session View Animation

Nucleic-Session: A8FFA1A6-443F-444A-A735-C53958D7E570
Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
2026-07-06 18:01:12 -07:00
co-authored by Nucleic
parent 70557737cd
commit 12893fdc2d
@@ -167,26 +167,54 @@ struct MarkdownText: View {
.lineSpacing(4) .lineSpacing(4)
} }
/// One prose line's classification: the exact string that gets inline-parsed — the cache key
/// `prewarm` must match — paired with how it's styled. Single source of truth for both the
/// live `lineView` and the off-main `prewarm`, so a warmed key can never drift from the key
/// the layout later looks up (any drift and the warm entry would silently miss).
private enum LineStyle {
case blank
case heading(content: String, scale: CGFloat, weight: Font.Weight)
case bullet(content: String)
case plain(content: String)
/// The string handed to `inline(_:)`, or nil for a line that renders without a parse.
var inlineContent: String? {
switch self {
case .blank: return nil
case .heading(let c, _, _), .bullet(let c), .plain(let c): return c
}
}
}
/// Classify one raw prose line. Headings are checked before bullets (a heading marker wins),
/// and the plain case keeps the *raw* line (not the trimmed one) exactly as the old cascade
/// did — the inline parser preserves leading whitespace under `.inlineOnlyPreservingWhitespace`.
private static func classify(_ raw: String) -> LineStyle {
let trimmed = raw.trimmingCharacters(in: .whitespaces)
if trimmed.isEmpty { return .blank }
// Headings scale relative to the base prose size so the hierarchy holds at any base and
// never collapses to the body size.
if trimmed.hasPrefix("### ") { return .heading(content: String(trimmed.dropFirst(4)), scale: 1.13, weight: .semibold) }
if trimmed.hasPrefix("## ") { return .heading(content: String(trimmed.dropFirst(3)), scale: 1.28, weight: .bold) }
if trimmed.hasPrefix("# ") { return .heading(content: String(trimmed.dropFirst(2)), scale: 1.5, weight: .bold) }
if let bullet = bulletContent(trimmed) { return .bullet(content: bullet) }
return .plain(content: raw)
}
@ViewBuilder @ViewBuilder
private func lineView(_ raw: String) -> some View { private func lineView(_ raw: String) -> some View {
let trimmed = raw.trimmingCharacters(in: .whitespaces) switch Self.classify(raw) {
if trimmed.isEmpty { case .blank:
Color.clear.frame(height: 3) Color.clear.frame(height: 3)
} else if trimmed.hasPrefix("### ") { case .heading(let content, let scale, let weight):
// Headings scale relative to the base prose size so the hierarchy holds inline(content).font(.system(size: bodySize * scale, weight: weight))
// at any base and never collapses to the body size. case .bullet(let content):
inline(String(trimmed.dropFirst(4))).font(.system(size: bodySize * 1.13, weight: .semibold))
} else if trimmed.hasPrefix("## ") {
inline(String(trimmed.dropFirst(3))).font(.system(size: bodySize * 1.28, weight: .bold))
} else if trimmed.hasPrefix("# ") {
inline(String(trimmed.dropFirst(2))).font(.system(size: bodySize * 1.5, weight: .bold))
} else if let bullet = Self.bulletContent(trimmed) {
HStack(alignment: .firstTextBaseline, spacing: 6) { HStack(alignment: .firstTextBaseline, spacing: 6) {
Text("•").foregroundStyle(.secondary) Text("•").foregroundStyle(.secondary)
inline(bullet) inline(content)
} }
} else { case .plain(let content):
inline(raw) inline(content)
} }
} }