feat(draw): fold the d2 source into a "view source" disclosure (T-494)
The rendered diagram leads; the d2 source carries through the draw bus and folds into a collapsed ClideCollapserCard beneath the card (D-78 display- only). en+nl catalogs. Also switches the d2 test to a null-aware element. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,7 @@
|
|||||||
"conversation.label.agentThinking": { "translation": "agent thinking" },
|
"conversation.label.agentThinking": { "translation": "agent thinking" },
|
||||||
"conversation.label.image": { "translation": "image" },
|
"conversation.label.image": { "translation": "image" },
|
||||||
"conversation.label.drawing": { "translation": "drawing" },
|
"conversation.label.drawing": { "translation": "drawing" },
|
||||||
|
"conversation.draw.viewSource": { "translation": "view d2 source" },
|
||||||
"conversation.label.agentRun": { "translation": "agent run" },
|
"conversation.label.agentRun": { "translation": "agent run" },
|
||||||
"conversation.label.workflow": { "translation": "workflow" },
|
"conversation.label.workflow": { "translation": "workflow" },
|
||||||
"conversation.label.error": { "translation": "error" },
|
"conversation.label.error": { "translation": "error" },
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"conversation.label.agentThinking": { "translation": "agent denkt na" },
|
"conversation.label.agentThinking": { "translation": "agent denkt na" },
|
||||||
"conversation.label.image": { "translation": "afbeelding" },
|
"conversation.label.image": { "translation": "afbeelding" },
|
||||||
"conversation.label.drawing": { "translation": "tekening" },
|
"conversation.label.drawing": { "translation": "tekening" },
|
||||||
|
"conversation.draw.viewSource": { "translation": "d2-bron tonen" },
|
||||||
"conversation.label.agentRun": { "translation": "agent-uitvoering" },
|
"conversation.label.agentRun": { "translation": "agent-uitvoering" },
|
||||||
"conversation.label.workflow": { "translation": "workflow" },
|
"conversation.label.workflow": { "translation": "workflow" },
|
||||||
"conversation.label.error": { "translation": "fout" },
|
"conversation.label.error": { "translation": "fout" },
|
||||||
|
|||||||
@@ -720,6 +720,10 @@ class _ConversationTurn extends StatelessWidget {
|
|||||||
document: doc,
|
document: doc,
|
||||||
label: m.label,
|
label: m.label,
|
||||||
description: m.description,
|
description: m.description,
|
||||||
|
source: m.source,
|
||||||
|
sourceLabel: m.source == null
|
||||||
|
? null
|
||||||
|
: ClideSettings.i18n.string(context, 'conversation.draw.viewSource', namespace: 'builtin.claude', placeholder: 'view d2 source'),
|
||||||
// A data-lightbox element opens the whole drawing, zoomable (T-318).
|
// A data-lightbox element opens the whole drawing, zoomable (T-318).
|
||||||
onLightbox: () => ClideKernel.of(context).dialog.show<Object>(
|
onLightbox: () => ClideKernel.of(context).dialog.show<Object>(
|
||||||
(ctx, dismiss) => ClideLightbox(
|
(ctx, dismiss) => ClideLightbox(
|
||||||
|
|||||||
@@ -661,6 +661,7 @@ class ClaudeExtension extends ClideExtension {
|
|||||||
svg: svg,
|
svg: svg,
|
||||||
label: m.data['label'] as String?,
|
label: m.data['label'] as String?,
|
||||||
description: m.data['description'] as String?,
|
description: m.data['description'] as String?,
|
||||||
|
source: m.data['source'] as String?,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -196,7 +196,15 @@ final class ImageMessage extends ConversationItem {
|
|||||||
/// paints (already lowered from the doc's template / primitive source);
|
/// paints (already lowered from the doc's template / primitive source);
|
||||||
/// [label] / [description] are the optional card caption.
|
/// [label] / [description] are the optional card caption.
|
||||||
final class DrawingMessage extends ConversationItem {
|
final class DrawingMessage extends ConversationItem {
|
||||||
const DrawingMessage({required super.uuid, required super.timestamp, required super.isSidechain, required this.svg, this.label, this.description});
|
const DrawingMessage({
|
||||||
|
required super.uuid,
|
||||||
|
required super.timestamp,
|
||||||
|
required super.isSidechain,
|
||||||
|
required this.svg,
|
||||||
|
this.label,
|
||||||
|
this.description,
|
||||||
|
this.source,
|
||||||
|
});
|
||||||
|
|
||||||
/// The SVG document source the renderer paints.
|
/// The SVG document source the renderer paints.
|
||||||
final String svg;
|
final String svg;
|
||||||
@@ -204,6 +212,10 @@ final class DrawingMessage extends ConversationItem {
|
|||||||
/// Optional card-level caption (label + supporting description).
|
/// Optional card-level caption (label + supporting description).
|
||||||
final String? label, description;
|
final String? label, description;
|
||||||
|
|
||||||
|
/// Optional template source (e.g. the d2 diagram text) — shown in a collapsed
|
||||||
|
/// "view source" disclosure on the card when present (T-494).
|
||||||
|
final String? source;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'DrawingMessage(${label ?? '<svg>'})';
|
String toString() => 'DrawingMessage(${label ?? '<svg>'})';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,12 @@ Future<IpcResponse> _draw(IpcRequest req, MessagePublisher? Function() publisher
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
publish('cli', drawShowChannel, {'svg': svg, if (doc.label != null) 'label': doc.label, if (doc.description != null) 'description': doc.description});
|
publish('cli', drawShowChannel, {
|
||||||
|
'svg': svg,
|
||||||
|
if (doc.label != null) 'label': doc.label,
|
||||||
|
if (doc.description != null) 'description': doc.description,
|
||||||
|
// d2 cards carry their source so the card can offer a "view source" peek.
|
||||||
|
if (doc.template == 'd2' && doc.fields['source'] is String) 'source': doc.fields['source'],
|
||||||
|
});
|
||||||
return IpcResponse.ok(id: req.id, data: {'shown': true, if (doc.template != null) 'template': doc.template});
|
return IpcResponse.ok(id: req.id, data: {'shown': true, if (doc.template != null) 'template': doc.template});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ library;
|
|||||||
|
|
||||||
import 'package:clide/kernel/src/theme/tokens.dart';
|
import 'package:clide/kernel/src/theme/tokens.dart';
|
||||||
import 'package:clide/widgets/src/clide_card_metrics.dart';
|
import 'package:clide/widgets/src/clide_card_metrics.dart';
|
||||||
|
import 'package:clide/widgets/src/clide_collapser_card.dart';
|
||||||
import 'package:clide/widgets/src/clide_settings.dart';
|
import 'package:clide/widgets/src/clide_settings.dart';
|
||||||
import 'package:clide/widgets/src/clide_text.dart';
|
import 'package:clide/widgets/src/clide_text.dart';
|
||||||
import 'package:clide/widgets/src/svg/svg_painter.dart';
|
import 'package:clide/widgets/src/svg/svg_painter.dart';
|
||||||
@@ -22,12 +23,27 @@ import 'package:clide/src/svg/svg_node.dart';
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class DrawingCard extends StatelessWidget {
|
class DrawingCard extends StatelessWidget {
|
||||||
const DrawingCard({super.key, required this.document, this.label, this.description, this.images, this.onLightbox, this.maxHeight = 360});
|
const DrawingCard({
|
||||||
|
super.key,
|
||||||
|
required this.document,
|
||||||
|
this.label,
|
||||||
|
this.description,
|
||||||
|
this.images,
|
||||||
|
this.onLightbox,
|
||||||
|
this.source,
|
||||||
|
this.sourceLabel,
|
||||||
|
this.maxHeight = 360,
|
||||||
|
});
|
||||||
|
|
||||||
final SvgDocument document;
|
final SvgDocument document;
|
||||||
final String? label, description;
|
final String? label, description;
|
||||||
final SvgImageResolver? images;
|
final SvgImageResolver? images;
|
||||||
|
|
||||||
|
/// The template source (e.g. d2 diagram text) and its disclosure title. When
|
||||||
|
/// [source] is present, a collapsed "view source" disclosure renders beneath
|
||||||
|
/// the drawing (T-494) — the rendered diagram leads; the code folds away.
|
||||||
|
final String? source, sourceLabel;
|
||||||
|
|
||||||
/// Called when a `data-lightbox` element is tapped (the caller opens the
|
/// Called when a `data-lightbox` element is tapped (the caller opens the
|
||||||
/// zoom view). Null ⇒ no lightbox affordance.
|
/// zoom view). Null ⇒ no lightbox affordance.
|
||||||
final VoidCallback? onLightbox;
|
final VoidCallback? onLightbox;
|
||||||
@@ -51,6 +67,20 @@ class DrawingCard extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.only(top: kClideCardHeaderPadV),
|
padding: const EdgeInsets.only(top: kClideCardHeaderPadV),
|
||||||
child: ClideText(description!, fontSize: clideFontCaption, color: tokens.globalTextMuted),
|
child: ClideText(description!, fontSize: clideFontCaption, color: tokens.globalTextMuted),
|
||||||
),
|
),
|
||||||
|
if (_has(source))
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: kClideCardHeaderPadV),
|
||||||
|
child: ClideCollapserCard(
|
||||||
|
label: sourceLabel ?? 'view source',
|
||||||
|
color: tokens.globalTextMuted,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(kClideCardHeaderPadH),
|
||||||
|
child: ClideText(source!, fontSize: clideFontCaption, fontFamily: ClideSettings.fonts.monoOf(context), color: tokens.globalForeground),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:clide/src/draw/draw_doc.dart';
|
|||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
DrawingCardDoc d2doc(Object? source) => DrawingCardDoc(template: 'd2', fields: {'template': 'd2', if (source != null) 'source': source});
|
DrawingCardDoc d2doc(Object? source) => DrawingCardDoc(template: 'd2', fields: {'template': 'd2', 'source': ?source});
|
||||||
|
|
||||||
group('d2TemplateHandler', () {
|
group('d2TemplateHandler', () {
|
||||||
test('compiles the source field via the injected compiler', () async {
|
test('compiles the source field via the injected compiler', () async {
|
||||||
|
|||||||
@@ -44,6 +44,29 @@ void main() {
|
|||||||
expect(find.text('the entry point'), findsOneWidget);
|
expect(find.text('the entry point'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('a source disclosure folds the d2 source under a collapser (T-494)', (tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
anchoredHarness(
|
||||||
|
f,
|
||||||
|
SizedBox(
|
||||||
|
width: 400,
|
||||||
|
child: DrawingCard(
|
||||||
|
document: buildSvgDocument('<svg viewBox="0 0 20 10"><rect width="20" height="10"/></svg>'),
|
||||||
|
source: 'a -> b: hello',
|
||||||
|
sourceLabel: 'view d2 source',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
// The disclosure header shows; the source is folded away until expanded.
|
||||||
|
expect(find.text('view d2 source'), findsOneWidget);
|
||||||
|
expect(find.text('a -> b: hello'), findsNothing);
|
||||||
|
await tester.tap(find.text('view d2 source'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('a -> b: hello'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('a data-lightbox element fires onLightbox when tapped (T-318)', (tester) async {
|
testWidgets('a data-lightbox element fires onLightbox when tapped (T-318)', (tester) async {
|
||||||
var tapped = false;
|
var tapped = false;
|
||||||
final doc = buildSvgDocument('<svg viewBox="0 0 100 50"><rect x="0" y="0" width="100" height="50" data-lightbox=""/></svg>');
|
final doc = buildSvgDocument('<svg viewBox="0 0 100 50"><rect x="0" y="0" width="100" height="50" data-lightbox=""/></svg>');
|
||||||
|
|||||||
Reference in New Issue
Block a user