add ClideMarkdown, ClideCodeBlock, ClideSvgView primitives
Three reusable rendering widgets in lib/widgets/: - ClideMarkdown: walks markdown AST (GFM tables, task lists, fenced code), renders headings, lists, blockquotes, tables, inline bold/italic/code/links. Delegates code blocks to ClideCodeBlock. - ClideCodeBlock: syntax-highlighted code via TreeSitterService. Async highlight, byte-to-char offset mapping, theme-aware colors from SurfaceTokens. - ClideSvgView: wraps jovial_svg with asset/string constructors and optional sizing. Retrofitted: decision detail body renders as markdown instead of raw text. Markdown viewer uses ClideMarkdown. Welcome logo uses ClideSvgView. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -108,15 +108,7 @@ class _DecisionDetailViewState extends State<DecisionDetailView> {
|
|||||||
),
|
),
|
||||||
if (body != null && body.isNotEmpty) ...[
|
if (body != null && body.isNotEmpty) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Container(
|
ClideMarkdown(body),
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: tokens.panelBackground,
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
border: Border.all(color: tokens.panelBorder),
|
|
||||||
),
|
|
||||||
child: ClideText(body, fontSize: 13, fontFamily: clideMonoFamily),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
if (refs.isNotEmpty) ...[
|
if (refs.isNotEmpty) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|||||||
@@ -78,16 +78,12 @@ class _MarkdownViewerState extends State<MarkdownViewer> {
|
|||||||
child: ClideText('Open a .md file to preview it here.', muted: true),
|
child: ClideText('Open a .md file to preview it here.', muted: true),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final tokens = ClideTheme.of(context).surface;
|
|
||||||
return ClidePaneChrome(
|
return ClidePaneChrome(
|
||||||
title: _path ?? 'viewer',
|
title: _path ?? 'viewer',
|
||||||
subtitle: '${_content!.split('\n').length} lines',
|
subtitle: '${_content!.split('\n').length} lines',
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
child: Text(
|
child: ClideMarkdown(_content!),
|
||||||
_content!,
|
|
||||||
style: TextStyle(color: tokens.globalForeground, fontSize: 13, fontFamily: clideMonoFamily, fontFamilyFallback: clideMonoFamilyFallback),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:clide/kernel/kernel.dart';
|
import 'package:clide/kernel/kernel.dart';
|
||||||
import 'package:clide/widgets/widgets.dart';
|
import 'package:clide/widgets/widgets.dart';
|
||||||
import 'package:flutter/services.dart' show rootBundle;
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:jovial_svg/jovial_svg.dart';
|
|
||||||
|
|
||||||
class WelcomeView extends StatelessWidget {
|
class WelcomeView extends StatelessWidget {
|
||||||
const WelcomeView({super.key});
|
const WelcomeView({super.key});
|
||||||
@@ -56,13 +54,7 @@ class _Header extends StatelessWidget {
|
|||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
const ClideSvgView.asset('assets/logo/logo.svg', width: 144, height: 144),
|
||||||
width: 144,
|
|
||||||
height: 144,
|
|
||||||
child: ScalableImageWidget.fromSISource(
|
|
||||||
si: ScalableImageSource.fromSvg(rootBundle, 'assets/logo/logo.svg'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 24),
|
const SizedBox(width: 24),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:clide/kernel/src/syntax/tree_sitter_service.dart';
|
||||||
|
import 'package:clide/kernel/src/theme/controller.dart';
|
||||||
|
import 'package:clide/widgets/src/typography.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class ClideCodeBlock extends StatefulWidget {
|
||||||
|
const ClideCodeBlock({super.key, required this.source, this.language});
|
||||||
|
|
||||||
|
final String source;
|
||||||
|
final String? language;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ClideCodeBlock> createState() => _ClideCodeBlockState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ClideCodeBlockState extends State<ClideCodeBlock> {
|
||||||
|
final TreeSitterService _syntax = TreeSitterService();
|
||||||
|
List<SyntaxSpan>? _spans;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_highlight();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(ClideCodeBlock old) {
|
||||||
|
super.didUpdateWidget(old);
|
||||||
|
if (old.source != widget.source || old.language != widget.language) {
|
||||||
|
_highlight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_syntax.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _highlight() async {
|
||||||
|
final lang = widget.language;
|
||||||
|
if (lang == null || lang.isEmpty) {
|
||||||
|
setState(() => _spans = null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final path = 'code.$lang';
|
||||||
|
if (!await _syntax.hasGrammar(path)) {
|
||||||
|
setState(() => _spans = null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final result = await _syntax.highlight(path, widget.source);
|
||||||
|
if (mounted) setState(() => _spans = result.spans);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final tokens = ClideTheme.of(context).surface;
|
||||||
|
final style = TextStyle(
|
||||||
|
fontFamily: clideMonoFamily,
|
||||||
|
fontFamilyFallback: clideMonoFamilyFallback,
|
||||||
|
fontSize: clideFontMono,
|
||||||
|
color: tokens.globalForeground,
|
||||||
|
);
|
||||||
|
|
||||||
|
final spans = _spans;
|
||||||
|
TextSpan textSpan;
|
||||||
|
|
||||||
|
if (spans == null || spans.isEmpty) {
|
||||||
|
textSpan = TextSpan(text: widget.source, style: style);
|
||||||
|
} else {
|
||||||
|
textSpan = _buildHighlightedSpan(widget.source, spans, style, tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: tokens.panelBackground,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(color: tokens.panelBorder),
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: RichText(text: textSpan),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static TextSpan _buildHighlightedSpan(String source, List<SyntaxSpan> spans, TextStyle base, dynamic tokens) {
|
||||||
|
final bytes = utf8.encode(source);
|
||||||
|
final byteToChar = List<int>.filled(bytes.length + 1, source.length);
|
||||||
|
var bi = 0;
|
||||||
|
for (var ci = 0; ci < source.length; ci++) {
|
||||||
|
byteToChar[bi] = ci;
|
||||||
|
final rune = source.codeUnitAt(ci);
|
||||||
|
if (rune < 0x80) {
|
||||||
|
bi += 1;
|
||||||
|
} else if (rune < 0x800) {
|
||||||
|
bi += 2;
|
||||||
|
} else if (rune >= 0xD800 && rune <= 0xDBFF) {
|
||||||
|
bi += 4;
|
||||||
|
ci++;
|
||||||
|
} else {
|
||||||
|
bi += 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
byteToChar[bi] = source.length;
|
||||||
|
|
||||||
|
final sorted = List.of(spans)..sort((a, b) => a.start.compareTo(b.start));
|
||||||
|
final children = <TextSpan>[];
|
||||||
|
var lastChar = 0;
|
||||||
|
|
||||||
|
for (final span in sorted) {
|
||||||
|
final sChar = span.start < byteToChar.length ? byteToChar[span.start] : source.length;
|
||||||
|
final eChar = span.end < byteToChar.length ? byteToChar[span.end] : source.length;
|
||||||
|
if (sChar > lastChar) {
|
||||||
|
children.add(TextSpan(text: source.substring(lastChar, sChar)));
|
||||||
|
}
|
||||||
|
if (eChar > sChar) {
|
||||||
|
final color = TreeSitterService.colorForRole(span.role, tokens);
|
||||||
|
children.add(TextSpan(text: source.substring(sChar, eChar), style: base.copyWith(color: color)));
|
||||||
|
}
|
||||||
|
lastChar = eChar;
|
||||||
|
}
|
||||||
|
if (lastChar < source.length) {
|
||||||
|
children.add(TextSpan(text: source.substring(lastChar)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return TextSpan(style: base, children: children);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
import 'package:clide/kernel/src/theme/controller.dart';
|
||||||
|
import 'package:clide/kernel/src/theme/tokens.dart';
|
||||||
|
import 'package:clide/widgets/src/clide_code_block.dart';
|
||||||
|
import 'package:clide/widgets/src/clide_divider.dart';
|
||||||
|
import 'package:clide/widgets/src/clide_text.dart';
|
||||||
|
import 'package:clide/widgets/src/typography.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:markdown/markdown.dart' as md;
|
||||||
|
|
||||||
|
class ClideMarkdown extends StatelessWidget {
|
||||||
|
const ClideMarkdown(this.source, {super.key});
|
||||||
|
|
||||||
|
final String source;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final tokens = ClideTheme.of(context).surface;
|
||||||
|
final doc = md.Document(extensionSet: md.ExtensionSet.gitHubFlavored);
|
||||||
|
final nodes = doc.parseLines(source.split('\n'));
|
||||||
|
final widgets = _buildNodes(nodes, tokens);
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: widgets,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Widget> _buildNodes(List<md.Node> nodes, SurfaceTokens tokens) {
|
||||||
|
final out = <Widget>[];
|
||||||
|
for (final node in nodes) {
|
||||||
|
if (node is md.Element) {
|
||||||
|
out.add(_buildElement(node, tokens));
|
||||||
|
} else if (node is md.Text) {
|
||||||
|
out.add(ClideText(node.text, fontSize: clideFontBody));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Widget _buildElement(md.Element el, SurfaceTokens tokens) {
|
||||||
|
switch (el.tag) {
|
||||||
|
case 'h1':
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16, bottom: 8),
|
||||||
|
child: _inlineText(el, tokens, fontSize: 22, fontWeight: FontWeight.w500),
|
||||||
|
);
|
||||||
|
case 'h2':
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 14, bottom: 6),
|
||||||
|
child: _inlineText(el, tokens, fontSize: 18, fontWeight: FontWeight.w500),
|
||||||
|
);
|
||||||
|
case 'h3':
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 12, bottom: 4),
|
||||||
|
child: _inlineText(el, tokens, fontSize: 16, fontWeight: FontWeight.w500),
|
||||||
|
);
|
||||||
|
case 'h4':
|
||||||
|
case 'h5':
|
||||||
|
case 'h6':
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 10, bottom: 4),
|
||||||
|
child: _inlineText(el, tokens, fontSize: clideFontBody, fontWeight: FontWeight.w600),
|
||||||
|
);
|
||||||
|
case 'p':
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: _inlineRichText(el, tokens),
|
||||||
|
);
|
||||||
|
case 'ul':
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16, bottom: 8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [for (final c in el.children ?? const []) if (c is md.Element) _buildListItem(c, tokens, ordered: false)],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case 'ol':
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16, bottom: 8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < (el.children?.length ?? 0); i++)
|
||||||
|
if (el.children![i] is md.Element) _buildListItem(el.children![i] as md.Element, tokens, ordered: true, index: i + 1),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case 'blockquote':
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 8),
|
||||||
|
padding: const EdgeInsets.only(left: 12),
|
||||||
|
decoration: BoxDecoration(border: Border(left: BorderSide(color: tokens.globalTextMuted, width: 3))),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: _buildNodes(el.children?.cast<md.Node>() ?? const [], tokens),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case 'pre':
|
||||||
|
final codeEl = el.children?.whereType<md.Element>().firstOrNull;
|
||||||
|
final code = codeEl?.textContent ?? el.textContent;
|
||||||
|
String? lang;
|
||||||
|
final cls = codeEl?.attributes['class'];
|
||||||
|
if (cls != null && cls.startsWith('language-')) {
|
||||||
|
lang = cls.substring(9);
|
||||||
|
}
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: ClideCodeBlock(source: code, language: lang),
|
||||||
|
);
|
||||||
|
case 'hr':
|
||||||
|
return Padding(padding: const EdgeInsets.symmetric(vertical: 8), child: ClideDivider());
|
||||||
|
case 'table':
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: _buildTable(el, tokens),
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return _inlineRichText(el, tokens);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static Widget _buildListItem(md.Element el, SurfaceTokens tokens, {bool ordered = false, int index = 1}) {
|
||||||
|
final bullet = ordered ? '$index. ' : '• ';
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
ClideText(bullet, color: tokens.globalTextMuted, fontSize: clideFontBody),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: _buildNodes(el.children?.cast<md.Node>() ?? const [], tokens),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Widget _buildTable(md.Element table, SurfaceTokens tokens) {
|
||||||
|
final rows = <TableRow>[];
|
||||||
|
for (final child in table.children ?? const []) {
|
||||||
|
if (child is! md.Element) continue;
|
||||||
|
for (final row in child.children ?? const []) {
|
||||||
|
if (row is! md.Element) continue;
|
||||||
|
final cells = <Widget>[];
|
||||||
|
final isHeader = row.tag == 'tr' && (child.tag == 'thead');
|
||||||
|
for (final cell in row.children ?? const []) {
|
||||||
|
if (cell is! md.Element) continue;
|
||||||
|
cells.add(Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
||||||
|
child: _inlineText(cell, tokens, fontWeight: isHeader ? FontWeight.w600 : null),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (cells.isNotEmpty) {
|
||||||
|
rows.add(TableRow(
|
||||||
|
decoration: isHeader ? BoxDecoration(border: Border(bottom: BorderSide(color: tokens.dividerColor))) : null,
|
||||||
|
children: cells,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rows.isEmpty) return const SizedBox.shrink();
|
||||||
|
return Table(
|
||||||
|
border: TableBorder.all(color: tokens.panelBorder, width: 1),
|
||||||
|
defaultVerticalAlignment: TableCellVerticalAlignment.top,
|
||||||
|
children: rows,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Widget _inlineText(md.Element el, SurfaceTokens tokens, {double? fontSize, FontWeight? fontWeight}) {
|
||||||
|
return RichText(text: _buildInlineSpan(el, tokens, fontSize: fontSize, fontWeight: fontWeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
static Widget _inlineRichText(md.Element el, SurfaceTokens tokens) {
|
||||||
|
return RichText(text: _buildInlineSpan(el, tokens));
|
||||||
|
}
|
||||||
|
|
||||||
|
static TextSpan _buildInlineSpan(md.Element el, SurfaceTokens tokens, {double? fontSize, FontWeight? fontWeight}) {
|
||||||
|
final children = <InlineSpan>[];
|
||||||
|
for (final child in el.children ?? const []) {
|
||||||
|
if (child is md.Text) {
|
||||||
|
children.add(TextSpan(text: child.text));
|
||||||
|
} else if (child is md.Element) {
|
||||||
|
children.add(_inlineElementSpan(child, tokens));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return TextSpan(
|
||||||
|
style: TextStyle(
|
||||||
|
color: tokens.globalForeground,
|
||||||
|
fontSize: fontSize ?? clideFontBody,
|
||||||
|
fontWeight: fontWeight,
|
||||||
|
),
|
||||||
|
children: children,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static TextSpan _inlineElementSpan(md.Element el, SurfaceTokens tokens) {
|
||||||
|
switch (el.tag) {
|
||||||
|
case 'strong':
|
||||||
|
return TextSpan(
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w700),
|
||||||
|
children: [for (final c in el.children ?? const []) if (c is md.Text) TextSpan(text: c.text) else if (c is md.Element) _inlineElementSpan(c, tokens)],
|
||||||
|
);
|
||||||
|
case 'em':
|
||||||
|
return TextSpan(
|
||||||
|
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||||
|
children: [for (final c in el.children ?? const []) if (c is md.Text) TextSpan(text: c.text) else if (c is md.Element) _inlineElementSpan(c, tokens)],
|
||||||
|
);
|
||||||
|
case 'code':
|
||||||
|
return TextSpan(
|
||||||
|
text: el.textContent,
|
||||||
|
style: TextStyle(fontFamily: clideMonoFamily, fontSize: clideFontMono, color: tokens.syntaxString, backgroundColor: tokens.panelBackground),
|
||||||
|
);
|
||||||
|
case 'a':
|
||||||
|
return TextSpan(
|
||||||
|
text: el.textContent,
|
||||||
|
style: TextStyle(color: tokens.globalFocus),
|
||||||
|
);
|
||||||
|
case 'del':
|
||||||
|
return TextSpan(
|
||||||
|
text: el.textContent,
|
||||||
|
style: TextStyle(decoration: TextDecoration.lineThrough, color: tokens.globalTextMuted),
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return TextSpan(text: el.textContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import 'package:flutter/services.dart' show rootBundle;
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:jovial_svg/jovial_svg.dart';
|
||||||
|
|
||||||
|
class ClideSvgView extends StatelessWidget {
|
||||||
|
const ClideSvgView.asset(this.assetPath, {super.key, this.width, this.height}) : svgString = null;
|
||||||
|
const ClideSvgView.string(this.svgString, {super.key, this.width, this.height}) : assetPath = null;
|
||||||
|
|
||||||
|
final String? assetPath;
|
||||||
|
final String? svgString;
|
||||||
|
final double? width;
|
||||||
|
final double? height;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget child;
|
||||||
|
if (assetPath != null) {
|
||||||
|
child = ScalableImageWidget.fromSISource(
|
||||||
|
si: ScalableImageSource.fromSvg(rootBundle, assetPath!),
|
||||||
|
);
|
||||||
|
} else if (svgString != null) {
|
||||||
|
final si = ScalableImage.fromSvgString(svgString!);
|
||||||
|
child = ScalableImageWidget(si: si);
|
||||||
|
} else {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (width != null || height != null) {
|
||||||
|
child = SizedBox(width: width, height: height, child: child);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,11 @@ library;
|
|||||||
|
|
||||||
export 'src/clide_button.dart';
|
export 'src/clide_button.dart';
|
||||||
export 'src/clide_column_hat.dart';
|
export 'src/clide_column_hat.dart';
|
||||||
|
export 'src/clide_code_block.dart';
|
||||||
export 'src/clide_divider.dart';
|
export 'src/clide_divider.dart';
|
||||||
export 'src/clide_filter_box.dart';
|
export 'src/clide_filter_box.dart';
|
||||||
|
export 'src/clide_markdown.dart';
|
||||||
|
export 'src/clide_svg_view.dart';
|
||||||
export 'src/clide_icon.dart';
|
export 'src/clide_icon.dart';
|
||||||
export 'src/clide_icon_rail.dart';
|
export 'src/clide_icon_rail.dart';
|
||||||
export 'src/clide_palette.dart';
|
export 'src/clide_palette.dart';
|
||||||
|
|||||||
Reference in New Issue
Block a user