Skip to content

Commit bd74316

Browse files
imjustinliaoclaude
andcommitted
v2.40 - Add Block Type & Variation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d81c2df commit bd74316

17 files changed

Lines changed: 1857 additions & 288 deletions

frontend/src/components/app/views/BlockLabView.tsx

Lines changed: 448 additions & 62 deletions
Large diffs are not rendered by default.

frontend/src/components/block-types/BulletList.tsx

Lines changed: 86 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,69 +5,123 @@ import { FONTS } from "@/lib/constants"
55
import { getVariantStyles } from "./variantStyles"
66
import type { BlockVariant } from "@/types/block-types"
77
import type { ThemeColors } from "@/context/ThemeContext"
8+
import type { ListStyle } from "@/types/block"
89

910
interface BulletItem {
1011
text: string
1112
label?: string
13+
children?: BulletItem[]
1214
}
1315

1416
interface BulletListBlockProps {
1517
items: BulletItem[]
1618
ordered?: boolean
19+
listStyle?: ListStyle
20+
depth?: number
1721
variant?: BlockVariant
1822
COLORS: ThemeColors
1923
renderItem: (text: string) => React.ReactNode
2024
}
2125

22-
/** Bullet list or ordered steps. Agent decides bullet vs numbered. */
26+
/** Generate label for ordered list items based on style and index */
27+
function generateLabel(style: ListStyle, index: number): string {
28+
switch (style) {
29+
case "number":
30+
return `${index + 1}.`
31+
case "alphabet":
32+
return `${String.fromCharCode(97 + (index % 26))}.`
33+
case "alphabet_upper":
34+
return `${String.fromCharCode(65 + (index % 26))}.`
35+
case "roman":
36+
return `${toRoman(index + 1).toLowerCase()}.`
37+
case "roman_upper":
38+
return `${toRoman(index + 1)}.`
39+
default:
40+
return "•"
41+
}
42+
}
43+
44+
/** Convert number to Roman numeral */
45+
function toRoman(num: number): string {
46+
const values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1]
47+
const symbols = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"]
48+
let result = ""
49+
let remaining = num
50+
for (let i = 0; i < values.length; i++) {
51+
while (remaining >= values[i]) {
52+
result += symbols[i]
53+
remaining -= values[i]
54+
}
55+
}
56+
return result
57+
}
58+
59+
/** Bullet list or ordered steps. Agent decides bullet vs numbered + style. */
2360
export const BulletListBlock = React.memo(function BulletListBlock({
2461
items,
2562
ordered = false,
63+
listStyle,
64+
depth = 0,
2665
variant = "view",
2766
COLORS,
2867
renderItem,
2968
}: BulletListBlockProps) {
3069
const styles = getVariantStyles(COLORS)[variant]
3170
const gap = variant === "card" ? "6px" : "8px"
3271
const margin = variant === "card" ? "2px 0" : ordered ? "3px 0" : "4px 0"
72+
const indent = depth * 20
73+
74+
const effectiveStyle: ListStyle = listStyle ?? (ordered ? "number" : "bullet")
3375

3476
return (
3577
<>
3678
{items.map((item, i) => (
37-
<div
38-
key={i}
39-
style={{
40-
display: "flex",
41-
gap,
42-
margin,
43-
paddingLeft: "4px",
44-
}}
45-
>
46-
<span
47-
style={{
48-
color: COLORS.textTertiary,
49-
fontSize: styles.bullet.fontSize,
50-
lineHeight: styles.bullet.lineHeight,
51-
flexShrink: 0,
52-
...(ordered && item.label && {
53-
minWidth: "24px",
54-
textAlign: "right",
55-
}),
56-
}}
57-
>
58-
{ordered && item.label ? item.label : "•"}
59-
</span>
60-
<span
79+
<React.Fragment key={i}>
80+
<div
6181
style={{
62-
fontFamily: FONTS.sans,
63-
fontSize: styles.bullet.fontSize,
64-
color: styles.bullet.color,
65-
lineHeight: styles.bullet.lineHeight,
82+
display: "flex",
83+
gap,
84+
margin,
85+
paddingLeft: `${4 + indent}px`,
6686
}}
6787
>
68-
{renderItem(item.text)}
69-
</span>
70-
</div>
88+
<span
89+
style={{
90+
color: COLORS.textTertiary,
91+
fontSize: styles.bullet.fontSize,
92+
lineHeight: styles.bullet.lineHeight,
93+
flexShrink: 0,
94+
...(effectiveStyle !== "bullet" && {
95+
minWidth: "24px",
96+
textAlign: "right",
97+
}),
98+
}}
99+
>
100+
{item.label || generateLabel(effectiveStyle, i)}
101+
</span>
102+
<span
103+
style={{
104+
fontFamily: FONTS.sans,
105+
fontSize: styles.bullet.fontSize,
106+
color: styles.bullet.color,
107+
lineHeight: styles.bullet.lineHeight,
108+
}}
109+
>
110+
{renderItem(item.text)}
111+
</span>
112+
</div>
113+
{item.children && item.children.length > 0 && (
114+
<BulletListBlock
115+
items={item.children}
116+
ordered={effectiveStyle !== "bullet"}
117+
listStyle={effectiveStyle === "number" ? "alphabet" : effectiveStyle === "alphabet" ? "roman" : effectiveStyle}
118+
depth={depth + 1}
119+
variant={variant}
120+
COLORS={COLORS}
121+
renderItem={renderItem}
122+
/>
123+
)}
124+
</React.Fragment>
71125
))}
72126
</>
73127
)
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"use client"
2+
3+
import React from "react"
4+
import { FONTS } from "@/lib/constants"
5+
import type { BlockVariant } from "@/types/block-types"
6+
import type { ThemeColors } from "@/context/ThemeContext"
7+
8+
interface ConclusionBlockProps {
9+
title?: string
10+
content: string
11+
keyTakeaways?: string[]
12+
variant?: BlockVariant
13+
COLORS: ThemeColors
14+
renderContent?: (text: string) => React.ReactNode
15+
}
16+
17+
/** Conclusion / Summary block — accent-bordered summary with optional takeaways. */
18+
export const ConclusionBlock = React.memo(function ConclusionBlock({
19+
title = "Conclusion",
20+
content,
21+
keyTakeaways,
22+
variant = "view",
23+
COLORS,
24+
renderContent,
25+
}: ConclusionBlockProps) {
26+
const margin = variant === "card" ? "8px 0" : "20px 0"
27+
const fontSize = variant === "card" ? "12px" : "15px"
28+
const padding = variant === "card" ? "12px 14px" : "20px 24px"
29+
30+
return (
31+
<div
32+
style={{
33+
margin,
34+
padding,
35+
borderLeft: `3px solid ${COLORS.accent}`,
36+
background: "rgba(196,117,0,0.04)",
37+
borderRadius: "0 10px 10px 0",
38+
}}
39+
>
40+
<h4
41+
style={{
42+
fontFamily: FONTS.sans,
43+
fontSize: variant === "card" ? "13px" : "16px",
44+
fontWeight: 600,
45+
color: COLORS.textPrimary,
46+
margin: "0 0 10px",
47+
}}
48+
>
49+
{title}
50+
</h4>
51+
<p
52+
style={{
53+
fontFamily: FONTS.sans,
54+
fontSize,
55+
color: COLORS.textSecondary,
56+
lineHeight: 1.7,
57+
margin: 0,
58+
}}
59+
>
60+
{renderContent ? renderContent(content) : content}
61+
</p>
62+
{keyTakeaways && keyTakeaways.length > 0 && (
63+
<ul
64+
style={{
65+
margin: "12px 0 0",
66+
padding: "0 0 0 20px",
67+
display: "flex",
68+
flexDirection: "column",
69+
gap: "6px",
70+
}}
71+
>
72+
{keyTakeaways.map((item, i) => (
73+
<li
74+
key={i}
75+
style={{
76+
fontFamily: FONTS.sans,
77+
fontSize: variant === "card" ? "11px" : "14px",
78+
color: COLORS.textSecondary,
79+
lineHeight: 1.6,
80+
}}
81+
>
82+
{renderContent ? renderContent(item) : item}
83+
</li>
84+
))}
85+
</ul>
86+
)}
87+
</div>
88+
)
89+
})
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"use client"
2+
3+
import React from "react"
4+
import { FONTS } from "@/lib/constants"
5+
import type { BlockVariant } from "@/types/block-types"
6+
import type { ThemeColors } from "@/context/ThemeContext"
7+
8+
interface DateBlockProps {
9+
date: string
10+
format?: "short" | "long" | "relative"
11+
variant?: BlockVariant
12+
COLORS: ThemeColors
13+
}
14+
15+
function formatDate(dateStr: string, fmt: "short" | "long" | "relative"): string {
16+
const d = new Date(dateStr)
17+
if (Number.isNaN(d.getTime())) return dateStr
18+
19+
if (fmt === "relative") {
20+
const now = new Date()
21+
const diff = now.getTime() - d.getTime()
22+
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
23+
if (days === 0) return "Today"
24+
if (days === 1) return "Yesterday"
25+
if (days < 7) return `${days} days ago`
26+
if (days < 30) return `${Math.floor(days / 7)} weeks ago`
27+
if (days < 365) return `${Math.floor(days / 30)} months ago`
28+
return `${Math.floor(days / 365)} years ago`
29+
}
30+
31+
if (fmt === "long") {
32+
return d.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric" })
33+
}
34+
35+
return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" })
36+
}
37+
38+
/** Date display block — formatted date/time element. */
39+
export const DateBlock = React.memo(function DateBlock({
40+
date,
41+
format = "long",
42+
variant = "view",
43+
COLORS,
44+
}: DateBlockProps) {
45+
const fontSize = variant === "card" ? "11px" : "13px"
46+
47+
return (
48+
<time
49+
dateTime={date}
50+
style={{
51+
display: "block",
52+
fontFamily: FONTS.sans,
53+
fontSize,
54+
color: COLORS.textTertiary,
55+
margin: variant === "card" ? "4px 0" : "8px 0",
56+
letterSpacing: "0.02em",
57+
}}
58+
>
59+
{formatDate(date, format)}
60+
</time>
61+
)
62+
})

0 commit comments

Comments
 (0)