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
27 changes: 16 additions & 11 deletions Ivy.Docs.Tools/MarkdownConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ public static async Task ConvertAsync(string name, string relativePath, string a
codeBuilder.AppendLine("using Ivy.Views.Kanban;");
codeBuilder.AppendLine("using static Ivy.Views.Layout;");
codeBuilder.AppendLine("using static Ivy.Views.Text;");
codeBuilder.AppendLine("using Ivy.Views;");
if (appMeta.Imports != null)
{
foreach (var import in appMeta.Imports)
Expand Down Expand Up @@ -194,15 +195,17 @@ private static void HandleBlocks(MarkdownDocument document, StringBuilder codeBu
{
var sectionBuilder = new StringBuilder();

void WriteSection()
void WriteSection(bool removeBottomMargin = false)
{
if (sectionBuilder.Length > 0)
{
var (types, convertedMarkdown) = linkConverter.Convert(sectionBuilder.ToString().Trim());
referencedApps.UnionWith(types);
AppendAsMultiLineStringIfNecessary(baseIndentLevel, convertedMarkdown, codeBuilder,
isNestedContent ? ", new Markdown(" : "| new Markdown(",
").HandleLinkClick(onLinkClick)");
removeBottomMargin
? ").HandleLinkClick(onLinkClick).WithLayout().Margin(0, 0, 0, 0)"
: ").HandleLinkClick(onLinkClick).WithLayout().Margin(0, 0, 0, 4)");
sectionBuilder.Clear();
}
}
Expand Down Expand Up @@ -259,7 +262,8 @@ void WriteSection()

if (!isInsideDetailsBlock)
{
WriteSection();
// Always remove bottom margin from preceding text for code blocks to ensure visual continuity
WriteSection(true);
HandleCodeBlock(codeBlock, markdownContent, codeBuilder, viewBuilder, usedClassNames, isNestedContent, baseIndentLevel);
}
}
Expand Down Expand Up @@ -447,7 +451,7 @@ private static void HandleDetailsBlockDirect(StringBuilder codeBuilder, string h
{
// Multiple items - wrap in Vertical()
codeBuilder.AppendTab(3).AppendLine($"""| new Expandable("{summary}",""");
codeBuilder.AppendTab(4).AppendLine("Vertical()");
codeBuilder.AppendTab(4).AppendLine("Vertical().Gap(4)");
codeBuilder.Append(bodyOutput);
codeBuilder.AppendLine();
codeBuilder.AppendTab(3).AppendLine(")");
Expand Down Expand Up @@ -500,13 +504,13 @@ private static void HandleCalloutBlock(StringBuilder codeBuilder, XElement xml,
var (types, convertedContent) = linkConverter.Convert(content);
referencedApps.UnionWith(types);

AppendAsMultiLineStringIfNecessary(3, convertedContent, codeBuilder, "| new Callout(", $", icon:Icons.{icon}).HandleLinkClick(onLinkClick)");
AppendAsMultiLineStringIfNecessary(3, convertedContent, codeBuilder, "| new Callout(", $", icon:Icons.{icon}).HandleLinkClick(onLinkClick).WithLayout().Margin(0, 0, 0, 4)");
}

private static void HandleEmbedBlock(StringBuilder codeBuilder, XElement xml)
{
string url = xml.Attribute("Url")?.Value ?? throw new Exception("Embed block must have an Url attribute.");
codeBuilder.AppendTab(3).AppendLine($"""| new Embed("{url}")""");
codeBuilder.AppendTab(3).AppendLine($"""| new Embed("{url}").WithLayout().Margin(0, 0, 0, 4)""");
}

private static void HandleIngressBlock(StringBuilder codeBuilder, XElement xml, LinkConverter linkConverter, HashSet<string> referencedApps)
Expand All @@ -520,7 +524,7 @@ private static void HandleIngressBlock(StringBuilder codeBuilder, XElement xml,
var (types, convertedContent) = linkConverter.Convert(content);
referencedApps.UnionWith(types);

AppendAsMultiLineStringIfNecessary(3, convertedContent, codeBuilder, "| Lead(", ")");
AppendAsMultiLineStringIfNecessary(3, convertedContent, codeBuilder, "| Lead(", ").WithLayout().Margin(0, 0, 0, 4)");
}

private static string MapLanguageToEnum(string lang)
Expand Down Expand Up @@ -555,7 +559,7 @@ private static void HandleCodeBlock(FencedCodeBlock codeBlock, string markdownCo
else if (language == "terminal")
{
var lines = codeContent.Split('\n');
codeBuilder.AppendTab(baseIndentLevel).AppendLine((isNestedContent ? ", " : "| ") + "new Terminal() ");
codeBuilder.AppendTab(baseIndentLevel).AppendLine((isNestedContent ? ", " : "| ") + "new Terminal()");
foreach (var line in lines)
{
if (line.StartsWith('>'))
Expand All @@ -567,20 +571,21 @@ private static void HandleCodeBlock(FencedCodeBlock codeBlock, string markdownCo
codeBuilder.AppendTab(baseIndentLevel + 1).AppendLine($".AddOutput({FormatLiteral(line.Trim())})");
}
}
codeBuilder.AppendTab(baseIndentLevel + 1).AppendLine(".WithLayout().Margin(0, 0, 0, 4)");
}
else if (language == "mermaid")
{
// Handle Mermaid diagrams by wrapping them in Markdown widget with proper syntax
string mermaidBlock = $"```mermaid\n{codeContent}\n```";
AppendAsMultiLineStringIfNecessary(baseIndentLevel, mermaidBlock, codeBuilder,
isNestedContent ? ", new Markdown(" : "| new Markdown(",
").HandleLinkClick(onLinkClick)");
").HandleLinkClick(onLinkClick).WithLayout().Margin(0, 0, 0, 4)");
}
else
{
AppendAsMultiLineStringIfNecessary(baseIndentLevel, codeContent, codeBuilder,
isNestedContent ? ", Code(" : "| Code(",
$",{MapLanguageToEnum(language)})");
$",{MapLanguageToEnum(language)}).WithLayout().Margin(0, 0, 0, 4)");
}
}

Expand All @@ -606,7 +611,7 @@ void AppendTabbedDemo(StringBuilder cb, string code, string insert, string lang)
cb.AppendTab(baseIndentLevel + 1).AppendLine($"new Tab(\"Demo\", new Box().Content({insert})),");
AppendAsMultiLineStringIfNecessary(baseIndentLevel + 1, code, cb, "new Tab(\"Code\", new Code(", $",{MapLanguageToEnum(lang)}))")
;
cb.AppendTab(baseIndentLevel).AppendLine(").Height(Size.Fit()).Padding(0, 8, 0, 0).Variant(TabsVariant.Content)");
cb.AppendTab(baseIndentLevel).AppendLine(").Height(Size.Fit()).Padding(0, 0, 0, 0).Variant(TabsVariant.Content)");
}

void AppendVerticalDemo(StringBuilder cb, string code, string insert, string lang, bool demoBelow)
Expand Down
95 changes: 95 additions & 0 deletions Ivy.Samples.Shared/Apps/Tests/TypographyTestApp.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using Ivy.Shared;
using Ivy.Samples.Shared.Apps;
using Ivy.Views;
using Ivy.Widgets;

namespace Ivy.Samples.Shared.Apps.Tests;

[App(icon: Icons.Airplay, path: ["Tests"], searchHints: ["typography", "text", "markdown", "comparison"])]
public class TypographyComparisonApp : SampleBase
{
protected override object? BuildSample()
{
var markdown = """
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6

This is a paragraph of text to compare the typography. It should have comfortable line height and spacing.

This is a paragraph of text to compare the typography. It should have comfortable line height and spacing.

* Unordered List Item 1
* Unordered List Item 2

1. Ordered List Item 1
2. Ordered List Item 2

> This is a blockquote. It should stand out from the rest of the text.

**Bold Text** and *Italic Text* and `Inline Code`.

---

End of the typography test.
""";

var html = """
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>

<p>This is a paragraph of text to compare the typography. It should have comfortable line height and spacing.</p>
<p>This is a paragraph of text to compare the typography. It should have comfortable line height and spacing.</p>

<ul>
<li>Unordered List Item 1</li>
<li>Unordered List Item 2</li>
</ul>

<ol>
<li>Ordered List Item 1</li>
<li>Ordered List Item 2</li>
</ol>

<blockquote>This is a blockquote. It should stand out from the rest of the text.</blockquote>

<p><strong>Bold Text</strong> and <em>Italic Text</em> and <code>Inline Code</code>.</p>

<hr />

<p>End of the typography test.</p>
""";

return Layout.Grid().Columns(3).Gap(20)
| new Card(new Markdown(markdown)).Title("Markdown Rendering")
| new Card(new Html(html)).Title("HTML Rendering")
| new Card(
Layout.Vertical().Gap(0)
| Text.H1("Heading 1")
| Text.H2("Heading 2")
| Text.H3("Heading 3")
| Text.H4("Heading 4")
| Text.H5("Heading 5")
| Text.H6("Heading 6")
| Text.P("This is a paragraph of text to compare the typography. It should have comfortable line height and spacing.")
| Text.P("This is a paragraph of text to compare the typography. It should have comfortable line height and spacing.")
| Text.Blockquote("This is a blockquote. It should stand out from the rest of the text.")
| (Layout.Horizontal().Gap(4)
| Text.Bold("Bold Text")
| Text.P("and")
| Text.Inline("Italic Text").Italic()
| Text.P("and")
| Text.InlineCode("Inline Code").Color(Colors.Red)
)
| new Separator()
| Text.P("End of the typography test.")
).Title("Text Widgets Rendering");
}
}
30 changes: 26 additions & 4 deletions Ivy/Widgets/Card.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@ public enum CardHoverVariant

public record Card : WidgetBase<Card>
{
public Card(object? content = null, object? footer = null, object? header = null) : base([new Slot("Content", content), new Slot("Footer", footer!), new Slot("Header", header!)])
public Card(object? content = null, object? footer = null, object? header = null) : base(
new List<object?>
{
content != null ? new Slot("Content", content) : null,
footer != null ? new Slot("Footer", footer) : null,
header != null ? new Slot("Header", header) : null
}.Where(x => x != null).Cast<object>().ToArray())
{
Width = Ivy.Shared.Size.Full();
}
Expand Down Expand Up @@ -43,13 +49,21 @@ internal Card() { }
{
throw new NotSupportedException("Cards does not support multiple children.");
}
return widget with { Children = [new Slot("Content", child), widget.GetSlot("Footer"), widget.GetSlot("Header")] };

var slots = new List<object?>
{
new Slot("Content", child),
widget.GetSlot("Footer"),
widget.GetSlot("Header")
};

return widget with { Children = slots.Where(x => x != null).Cast<object>().ToArray() };
}
}

public static class CardExtensions
{
internal static Slot GetSlot(this Card card, string name) => card.Children.FirstOrDefault(e => e is Slot slot && slot.Name == name) as Slot ?? new Slot(name, null!);
internal static Slot? GetSlot(this Card card, string name) => card.Children.FirstOrDefault(e => e is Slot slot && slot.Name == name) as Slot;

public static Card Header(this Card card, object? title = null, object? description = null, object? icon = null)
{
Expand All @@ -58,9 +72,17 @@ public static Card Header(this Card card, object? title = null, object? descript
| title?.WithLayout().Grow()
| icon)
| description;

var slots = new List<object?>
{
card.GetSlot("Content"),
card.GetSlot("Footer"),
header != null ? new Slot("Header", header) : null
};

return card with
{
Children = [card.GetSlot("Content"), card.GetSlot("Footer"), new Slot("Header", header)],
Children = slots.Where(x => x != null).Cast<object>().ToArray(),
Title = title,
Description = description,
Icon = icon
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/MarkdownRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,6 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
td: memo(({ children }: { children: React.ReactNode }) => (
<td className={typography.td}>{children}</td>
)),
hr: memo(() => <hr className="my-6" />),
img: memo(
(props: React.ImgHTMLAttributes<HTMLImageElement>) => {
const [showOverlay, setShowOverlay] = useState(false);
Expand Down Expand Up @@ -385,6 +384,9 @@ const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
(prevProps, nextProps) =>
prevProps.src === nextProps.src && prevProps.alt === nextProps.alt
),
hr: memo((props: React.HTMLAttributes<HTMLHRElement>) => (
<hr className={typography.hr} {...props} />
)),
}),
[]
);
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/lib/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ export const typography: Record<string, string> = {
h6: `text-base font-medium scroll-m-20 mt-2 mb-2 [&+p]:mt-0`,

// Body
p: `text-base scroll-m-20 my-4 [&+p]:mt-0`,
p: `text-base scroll-m-20 mb-4`,
lead: `text-muted-foreground`,
strong: 'font-semibold',
em: 'italic',
Expand Down Expand Up @@ -605,18 +605,19 @@ export const typography: Record<string, string> = {
a: 'text-primary underline brightness-90 hover:brightness-100',

// Blockquote
blockquote: 'border-l-2 pl-6 italic mb-2',
blockquote: 'border-l-2 pl-6 italic mb-4',

// Code
code: 'relative rounded bg-muted px-[0.25rem] py-[0.05rem] font-mono text-sm font-semibold',

// Table
table: 'w-full border-collapse border border-border mb-2',
table: 'w-full border-collapse border border-border mb-4',
thead: 'bg-muted',
tr: 'border border-border',
th: 'border border-border px-4 py-2 text-left font-bold text-sm',
td: 'border border-border px-4 py-2 text-sm',

// Media
img: 'max-w-full h-auto cursor-zoom-in mb-2',
img: 'max-w-full h-auto cursor-zoom-in mb-4',
hr: 'my-6 border-t border-border',
};
14 changes: 2 additions & 12 deletions frontend/src/widgets/card/CardWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import {
import { cn } from '@/lib/utils';
import { useEventHandler } from '@/components/event-handler';
import React, { useCallback } from 'react';
import { EmptyWidget } from '../primitives/EmptyWidget';
import { Scales } from '@/types/scale';
import { cardStyles, getSizeClasses } from './styles';

Expand Down Expand Up @@ -66,17 +65,8 @@ export const CardWidget: React.FC<CardWidgetProps> = ({
...(borderColor && getColor(borderColor, 'borderColor', 'background')),
};

const footerIsEmpty =
slots?.Footer?.length === 0 ||
slots?.Footer?.some(
node => React.isValidElement(node) && node.type === EmptyWidget
);

const headerIsEmpty =
slots?.Header?.length === 0 ||
slots?.Header?.some(
node => React.isValidElement(node) && node.type === EmptyWidget
);
const footerIsEmpty = !slots?.Footer || slots.Footer.length === 0;
const headerIsEmpty = !slots?.Header || slots.Header.length === 0;

const handleClick = useCallback(() => {
if (events.includes('OnClick')) eventHandler('OnClick', id, []);
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/widgets/primitives/CalloutWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const CalloutWidget: React.FC<CalloutWidgetProps> = ({
<div
style={styles}
className={cn(
'flex items-center px-4 my-4 text-large-body rounded-lg border transition-colors',
'flex items-center px-4 text-large-body rounded-lg border transition-colors',
variantStyles.container
)}
role="alert"
Expand All @@ -79,10 +79,12 @@ export const CalloutWidget: React.FC<CalloutWidgetProps> = ({
/>
)}
<span className="sr-only">{variant}</span>
<div className="flex flex-col min-w-0 flex-1">
<div className="flex flex-col min-w-0 flex-1 py-4">
{title && <div className="font-medium leading-none mb-1">{title}</div>}
{children && (
<div className="text-sm opacity-90 [&_p]:text-sm">{children}</div>
<div className="text-sm opacity-90 [&_p]:text-sm [&_p]:mb-0">
{children}
</div>
)}
</div>
</div>
Expand Down
12 changes: 7 additions & 5 deletions frontend/src/widgets/primitives/MarkdownWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ const MarkdownWidget: React.FC<MarkdownWidgetProps> = ({
);

return (
<MarkdownRenderer
key={id}
content={content}
onLinkClick={handleLinkClick}
/>
<div className="markdown-widget w-full">
<MarkdownRenderer
key={id}
content={content}
onLinkClick={handleLinkClick}
/>
</div>
);
};

Expand Down
Loading
Loading