The unified drawing card's primitive layer is SVG, not a bespoke
{type:"rect"} JSON vocabulary; the clide-owned CustomPaint SVG renderer
(T-320) is the engine the rest builds on. Templates lower to SVG; a thin
Flutter overlay carries label/description captions + lightbox, anchored
via data-* attributes; the graph template stays a live widget. Inverts
the former T-320 → T-318 dependency. Raised by the user: "if we are close
to svg, are we not better off extending svg instead?"
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
INSERTINTOticket_history(ticket_record_id,field,old_value,new_value,changed_by,changed_at,created_at,updated_at,deleted_at,hash,canonical_version)VALUES('06FB2ACSDBDZARV3NNGYD9NYYR','description','Add a stdin path to clide''s CLI so a command can receive a JSON payload piped in — `… | clide icon show --stdin`, `cat meta.json | clide image show foo.png --stdin` — instead of only positionals/flags or a `--file`.
##Why
Structuredcommands(thelabelledicon-cardentriesinT-313,imageannotationmetadatainT-316)wantaJSONpayloadthat''sawkwardtoexpressasflags.Todayclide''sCLIargvparser(lib/src/cli/argv_to_request.dart)onlyproducespositionals,--flags, and `-- passthrough`; there is no stdin path. T-313 therefore falls back to a `--file <path.json>` flag. A `--stdin` convention is the ergonomic peer of `--file` for piping, and is shared infra both icon.show and image.show consume.
Unblocksthepiped-JSONvariantsofT-313(iconentries)andT-316(imageannotations);bothcanalsoshipwith--file independently of this.', 'Add a stdin path to clide''s CLI so a command can receive a JSON payload piped in — `… | clide icon show --stdin`, `cat meta.json | clide image show foo.png --stdin` — instead of only positionals/flags or a `--file`.
##Why
Structuredcommands(thelabelledicon-cardentriesinT-313,imageannotationmetadatainT-316)wantaJSONpayloadthat''sawkwardtoexpressasflags.Todayclide''sCLIargvparser(lib/src/cli/argv_to_request.dart)onlyproducespositionals,--flags, and `-- passthrough`; there is no stdin path. T-313 therefore falls back to a `--file <path.json>` flag. A `--stdin` convention is the ergonomic peer of `--file` for piping, and is shared infra both icon.show and image.show consume.
Unblocksthepiped-JSONvariantsofT-313(iconentries)andT-316(imageannotations);bothcanalsoshipwith--file independently of this.
Deferred2026-06-28(user):blockedonitsconsumersT-316+T-313soitonlyresurfacesifoneactuallywantsthepipingUX.Standalone--stdin infra isn''t worth a build cycle now — both consumers can ship with --file (per this ticket), and structured JSON also flows natively through the MCP tool surface (D-86). Build it lazily inside whichever consumer first needs piping, if ever.', NULL, '2026-06-28 13:28:49', '2026-06-28 13:28:49.359', '2026-06-28 13:28:49.359', NULL, '890b0e8efb157c36d1acc26d22876b81', 2) ON CONFLICT(hash) DO NOTHING;
INSERTINTOticket_history(ticket_record_id,field,old_value,new_value,changed_by,changed_at,created_at,updated_at,deleted_at,hash,canonical_version)VALUES('06FB234WP4Y6Q16A0HFW8BSXMG','description','A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane — and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
-LABELLED/DESCRIBEDentries:a`--file <path.json>` flag whose value is a JSON array of `{"icon": "gear", "label": "Settings", "description": "global scope"}` (label, description optional); the handler reads and parses the file. NOTE: do NOT spec `--stdin` — clide''s CLI argv parser (lib/src/cli/argv_to_request.dart) only produces positionals/flags/passthrough and has no stdin path, so a `--file` flag (or repeated flags) is the grounded choice unless we deliberately add stdin support as separate work.
-RENDERPATH:validate+resolveiconnamesinthehandler(injectaresolverthewayimage.showinjectsImagePathResolver,soheadless/dart-teststaysfilesystem-free),thenpublishonadedicatedMessageBuschannel—e.g.`iconShowChannel=''icon''`,peerof`imageShowChannel=''image''`—capturedpost-bootinmain.dart;theClaudeextensionsubscribestothatliteralandinjectsthecardintotheprimarysession''sconversationlog.Honestfailure(IpcErroruserError/notFound)onanunknownglyphnameoramalformed/missing--file, and on no live UI bus (headless), mirroring image.show.
-A`--file <path.json>` payload lets each icon carry an optional label and optional description, both rendered alongside the glyph (no --stdin — not supported by the CLI parser).
FOLLOW-UPS:thepiped-JSON(--stdin) variant is split out as T-315 (generic CLI stdin plumbing); image.show gets the same metadata/annotation treatment in T-316. T-313 ships with --file regardless of T-315.', 'A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane — and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
-LABELLED/DESCRIBEDentries:a`--file <path.json>` flag whose value is a JSON array of `{"icon": "gear", "label": "Settings", "description": "global scope"}` (label, description optional); the handler reads and parses the file. NOTE: do NOT spec `--stdin` — clide''s CLI argv parser (lib/src/cli/argv_to_request.dart) only produces positionals/flags/passthrough and has no stdin path, so a `--file` flag (or repeated flags) is the grounded choice unless we deliberately add stdin support as separate work.
-RENDERPATH:validate+resolveiconnamesinthehandler(injectaresolverthewayimage.showinjectsImagePathResolver,soheadless/dart-teststaysfilesystem-free),thenpublishonadedicatedMessageBuschannel—e.g.`iconShowChannel=''icon''`,peerof`imageShowChannel=''image''`—capturedpost-bootinmain.dart;theClaudeextensionsubscribestothatliteralandinjectsthecardintotheprimarysession''sconversationlog.Honestfailure(IpcErroruserError/notFound)onanunknownglyphnameoramalformed/missing--file, and on no live UI bus (headless), mirroring image.show.
-A`--file <path.json>` payload lets each icon carry an optional label and optional description, both rendered alongside the glyph (no --stdin — not supported by the CLI parser).
FOLLOW-UPS:thepiped-JSON(--stdin) variant is split out as T-315 (generic CLI stdin plumbing); image.show gets the same metadata/annotation treatment in T-316. T-313 ships with --file regardless of T-315.
DESIGNREFINEMENT(2026-06-28,wireframereview):broadentheper-entrysize-samplestripbeyondclide''sinlinetypographytokens.clideiconshowisageneral-purposeglyphpreviewer,notboundtoclide''sownUI,sothestriprendersBOTHclideinlinetokens(11–15:badge/small/meta/caption/body)ANDcommoniconsizes(18,20,24,32,48).Herostays52(clideFontWelcomeBanner).Updatetheacceptancebulletaccordingly:''asampleateachinlinetoken(11–15)PLUScommonsizes18/20/24/32/48'',drivenoffnamedtokenswheretheyexistandexplicitpxotherwise.Wireframe:docs/design/wireframes/conversation/icon-glyph-card.png.', NULL, '2026-06-2813:43:39', '2026-06-2813:43:39.648', '2026-06-2813:43:39.648', NULL, '67c98f21d0dea3be7327403295906b7e', 2) ON CONFLICT(hash) DO NOTHING;
INSERTINTOticket_history(ticket_record_id,field,old_value,new_value,changed_by,changed_at,created_at,updated_at,deleted_at,hash,canonical_version)VALUES('06FB234WP4Y6Q16A0HFW8BSXMG','description','A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane — and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
-LABELLED/DESCRIBEDentries:a`--file <path.json>` flag whose value is a JSON array of `{"icon": "gear", "label": "Settings", "description": "global scope"}` (label, description optional); the handler reads and parses the file. NOTE: do NOT spec `--stdin` — clide''s CLI argv parser (lib/src/cli/argv_to_request.dart) only produces positionals/flags/passthrough and has no stdin path, so a `--file` flag (or repeated flags) is the grounded choice unless we deliberately add stdin support as separate work.
-RENDERPATH:validate+resolveiconnamesinthehandler(injectaresolverthewayimage.showinjectsImagePathResolver,soheadless/dart-teststaysfilesystem-free),thenpublishonadedicatedMessageBuschannel—e.g.`iconShowChannel=''icon''`,peerof`imageShowChannel=''image''`—capturedpost-bootinmain.dart;theClaudeextensionsubscribestothatliteralandinjectsthecardintotheprimarysession''sconversationlog.Honestfailure(IpcErroruserError/notFound)onanunknownglyphnameoramalformed/missing--file, and on no live UI bus (headless), mirroring image.show.
-A`--file <path.json>` payload lets each icon carry an optional label and optional description, both rendered alongside the glyph (no --stdin — not supported by the CLI parser).
FOLLOW-UPS:thepiped-JSON(--stdin) variant is split out as T-315 (generic CLI stdin plumbing); image.show gets the same metadata/annotation treatment in T-316. T-313 ships with --file regardless of T-315.
-LABELLED/DESCRIBEDentries:a`--file <path.json>` flag whose value is a JSON array of `{"icon": "gear", "label": "Settings", "description": "global scope"}` (label, description optional); the handler reads and parses the file. NOTE: do NOT spec `--stdin` — clide''s CLI argv parser (lib/src/cli/argv_to_request.dart) only produces positionals/flags/passthrough and has no stdin path, so a `--file` flag (or repeated flags) is the grounded choice unless we deliberately add stdin support as separate work.
-RENDERPATH:validate+resolveiconnamesinthehandler(injectaresolverthewayimage.showinjectsImagePathResolver,soheadless/dart-teststaysfilesystem-free),thenpublishonadedicatedMessageBuschannel—e.g.`iconShowChannel=''icon''`,peerof`imageShowChannel=''image''`—capturedpost-bootinmain.dart;theClaudeextensionsubscribestothatliteralandinjectsthecardintotheprimarysession''sconversationlog.Honestfailure(IpcErroruserError/notFound)onanunknownglyphnameoramalformed/missing--file, and on no live UI bus (headless), mirroring image.show.
-A`--file <path.json>` payload lets each icon carry an optional label and optional description, both rendered alongside the glyph (no --stdin — not supported by the CLI parser).
FOLLOW-UPS:thepiped-JSON(--stdin) variant is split out as T-315 (generic CLI stdin plumbing); image.show gets the same metadata/annotation treatment in T-316. T-313 ships with --file regardless of T-315.
SizesetFINALIZED(2026-06-28):onecontinuoussamplestrip—10,11,12,13,14,15,18,20,24,32,48px—noclide-vs-commonvisualsplit.Herostays52.Supersedesthegroupedframinginthepriornote.', NULL, '2026-06-2813:47:44', '2026-06-2813:47:44.006', '2026-06-2813:47:44.006', NULL, '89902262b44651cef22839749396391a', 2) ON CONFLICT(hash) DO NOTHING;
INSERTINTOticket_history(ticket_record_id,field,old_value,new_value,changed_by,changed_at,created_at,updated_at,deleted_at,hash,canonical_version)VALUES('06FB234WP4Y6Q16A0HFW8BSXMG','description','A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane — and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
-LABELLED/DESCRIBEDentries:a`--file <path.json>` flag whose value is a JSON array of `{"icon": "gear", "label": "Settings", "description": "global scope"}` (label, description optional); the handler reads and parses the file. NOTE: do NOT spec `--stdin` — clide''s CLI argv parser (lib/src/cli/argv_to_request.dart) only produces positionals/flags/passthrough and has no stdin path, so a `--file` flag (or repeated flags) is the grounded choice unless we deliberately add stdin support as separate work.
-RENDERPATH:validate+resolveiconnamesinthehandler(injectaresolverthewayimage.showinjectsImagePathResolver,soheadless/dart-teststaysfilesystem-free),thenpublishonadedicatedMessageBuschannel—e.g.`iconShowChannel=''icon''`,peerof`imageShowChannel=''image''`—capturedpost-bootinmain.dart;theClaudeextensionsubscribestothatliteralandinjectsthecardintotheprimarysession''sconversationlog.Honestfailure(IpcErroruserError/notFound)onanunknownglyphnameoramalformed/missing--file, and on no live UI bus (headless), mirroring image.show.
-A`--file <path.json>` payload lets each icon carry an optional label and optional description, both rendered alongside the glyph (no --stdin — not supported by the CLI parser).
FOLLOW-UPS:thepiped-JSON(--stdin) variant is split out as T-315 (generic CLI stdin plumbing); image.show gets the same metadata/annotation treatment in T-316. T-313 ships with --file regardless of T-315.
-LABELLED/DESCRIBEDentries:a`--file <path.json>` flag whose value is a JSON array of `{"icon": "gear", "label": "Settings", "description": "global scope"}` (label, description optional); the handler reads and parses the file. NOTE: do NOT spec `--stdin` — clide''s CLI argv parser (lib/src/cli/argv_to_request.dart) only produces positionals/flags/passthrough and has no stdin path, so a `--file` flag (or repeated flags) is the grounded choice unless we deliberately add stdin support as separate work.
-RENDERPATH:validate+resolveiconnamesinthehandler(injectaresolverthewayimage.showinjectsImagePathResolver,soheadless/dart-teststaysfilesystem-free),thenpublishonadedicatedMessageBuschannel—e.g.`iconShowChannel=''icon''`,peerof`imageShowChannel=''image''`—capturedpost-bootinmain.dart;theClaudeextensionsubscribestothatliteralandinjectsthecardintotheprimarysession''sconversationlog.Honestfailure(IpcErroruserError/notFound)onanunknownglyphnameoramalformed/missing--file, and on no live UI bus (headless), mirroring image.show.
-A`--file <path.json>` payload lets each icon carry an optional label and optional description, both rendered alongside the glyph (no --stdin — not supported by the CLI parser).
FOLLOW-UPS:thepiped-JSON(--stdin) variant is split out as T-315 (generic CLI stdin plumbing); image.show gets the same metadata/annotation treatment in T-316. T-313 ships with --file regardless of T-315.
COLOR(2026-06-28,foldedintopayloadspec):addanOPTIONALper-entry''color''tothe--file JSON, e.g. {"icon":"gear","label":"Settings","color":"#e2b714"}. Value is an ARBITRARY color — hex #rrggbb / #rrggbbaa (CSS-style names acceptable) — passed straight to PhosphorIconPainter. NOT a clide SurfaceTokens theme token: the glyph is content for whatever project we''re working on (its own palette), and the D-7 token discipline governs clide''s own chrome, not rendered content. Optional card-level default ''color''; a glyph with none falls back to the card''s default foreground. Unknown/malformed color → the same honest userError as a bad glyph name. Mirrors how arbitrary colors already flow through the image/svg cards.', NULL, '2026-06-28 14:00:47', '2026-06-28 14:00:47.466', '2026-06-28 14:00:47.466', NULL, '9d27fb1aca3e4b48131db84d55c7d2b1', 2) ON CONFLICT(hash) DO NOTHING;
INSERTINTOticket_history(ticket_record_id,field,old_value,new_value,changed_by,changed_at,created_at,updated_at,deleted_at,hash,canonical_version)VALUES('06FGX26B0NAC9WRJMVB0QE6CV8','description',NULL,'A conversation-pane drawing card (peer of the SVG card T-320) that renders a D2 diagram. SEPARATE card type from SVG, but the ONLY difference is a compile step in front: D2 source → compile to SVG → render through the SAME SVG widget (T-320). Blocked on T-320 for that reason.
CLI/parity(D-6):drivenviathedrawing-carddispatch(T-318)—''clidedraw--file pipeline.d2'' (type inferred from .d2), or template:"d2" with inline source in the drawing-card JSON. Honest userError on a d2 compile failure (bad source), mirroring image/svg validation.
Open:D2compilerdependency—d2isGo(thereisad2-diagramskill).Decidewhethertoshellouttoad2binary(supporter-toolpattern,peerofpql)orvendorit;flagbeforebuilding.Wireframe:docs/design/wireframes/conversation/d2-render-card.png.', NULL, '2026-06-2814:01:24', '2026-06-2814:01:24.709', '2026-06-2814:01:24.709', NULL, '0dfd716104a3b7defb4155f932f75c4c', 2) ON CONFLICT(hash) DO NOTHING;
INSERTINTOticket_history(ticket_record_id,field,old_value,new_value,changed_by,changed_at,created_at,updated_at,deleted_at,hash,canonical_version)VALUES('06FB2ERREMEEF26KKHGNZBWW64','description','Foundational build for the unified drawing card (epic T-317, decision D-91). A clide-owned canvas (CustomPaint) that renders from a JSON document: a PRIMITIVE scene-graph layer (rect/line/text/glyph/image at coordinates) plus a TEMPLATE-dispatch layer that maps a named component in the JSON to a predefined renderer. Templates lower onto the same primitive scene (hybrid model). Per drawn object, an optional label + description widget renders beneath it, only when those fields are present in the JSON. Display-only (D-78) — no inline selection. Driven via the clide CLI (D-6 parity), consuming the JSON input plumbing (T-315 / --file); mirror image.show''s Flutter-free handler + MessageBus publish + Claude-extension injection pattern. THIS ticket = the engine, the JSON schema, the primitive renderer, the dispatch mechanism, and the shared per-object label/description widget. Individual templates (image, icon, compare, svg, graph) are separate children. Acceptance: a JSON doc with raw primitives draws; a JSON doc naming a template dispatches to it; an object with label/description renders the caption widget; unknown template/primitive fails with a clear userError.','Foundational build for the unified drawing card (epic T-317, decision D-91). A clide-owned canvas (CustomPaint) that renders from a JSON document: a PRIMITIVE scene-graph layer (rect/line/text/glyph/image at coordinates) plus a TEMPLATE-dispatch layer that maps a named component in the JSON to a predefined renderer. Templates lower onto the same primitive scene (hybrid model). Per drawn object, an optional label + description widget renders beneath it, only when those fields are present in the JSON. Display-only (D-78) — no inline selection. Driven via the clide CLI (D-6 parity), consuming the JSON input plumbing (T-315 / --file); mirror image.show''s Flutter-free handler + MessageBus publish + Claude-extension injection pattern. THIS ticket = the engine, the JSON schema, the primitive renderer, the dispatch mechanism, and the shared per-object label/description widget. Individual templates (image, icon, compare, svg, graph) are separate children. Acceptance: a JSON doc with raw primitives draws; a JSON doc naming a template dispatches to it; an object with label/description renders the caption widget; unknown template/primitive fails with a clear userError.
SCHEMADRAFTED(2026-06-28):docs/design/drawing-card-schema.md—declarativeJSONscene-graph(documentenvelope,primitivetypesrect/line/text/glyph/image,templateenvelope,sharedlabel/description,arbitrary-hexcolor,CLI''clidedraw--file'', error contract). Refined from the T-317 wireframe set.
MODELCLARIFICATION(user):webuildclide''sOWNnativeFlutterCustomPaintinterpretingadeclarativescene-graphdocument.WedoNOTporttheHTMLCanvas2DAPI—''HTMLcanvas''inD-91isonlythementalmodel(ageneraldrawingsurface,chosentorejectObsidian''s.canvasschema),notanAPItoimplement.ThedeclarativemodelisclosertoSVG/aretainedscene-graphthantocanvas''simperative2Dcontext.', NULL, '2026-06-2815:20:03', '2026-06-2815:20:03.628', '2026-06-2815:20:03.628', NULL, 'a0c15cfea906b9896b19516bf3859bea', 2) ON CONFLICT(hash) DO NOTHING;
INSERTINTOticket_history(ticket_record_id,field,old_value,new_value,changed_by,changed_at,created_at,updated_at,deleted_at,hash,canonical_version)VALUES('06FB2ETJQP0CT6X7W3CWZ6NS9G','description','Template for the unified drawing card (T-317, D-91): render an SVG file referenced in the JSON. clide-owned rendering per the own-the-rendering-stack guardrail — no opinionated SVG package without justification; evaluate what is feasible via CustomPaint / a minimal path parser, and scope accordingly (SVG is a broad spec; define the supported subset). Optional label + description beneath (T-318). Display-only (D-78). Depends on the core engine (T-318).','Template for the unified drawing card (T-317, D-91): render an SVG file referenced in the JSON. clide-owned rendering per the own-the-rendering-stack guardrail — no opinionated SVG package without justification; evaluate what is feasible via CustomPaint / a minimal path parser, and scope accordingly (SVG is a broad spec; define the supported subset). Optional label + description beneath (T-318). Display-only (D-78). Depends on the core engine (T-318).
PROMOTEDTOENGINE(D-103,2026-06-28):thisisnowthedrawing-card''srenderingFOUNDATION,notonetemplateamongmany.Buildtheclide-ownedCustomPaintSVGrendereroveraBOUNDEDsubset(rect/line/polyline/polygon/circle/ellipse/path,textincl.thePhosphorfont,image,g+transform,basicfill/stroke/opacity/rx).T-318(envelope+dispatch+Flutteroverlay)andeverytemplatelowerontothis;d2/graphviz/mermaidoutputrendersforfree.NolongerblockedbyT-318(dependencyinverted).Open:pinthesubsettowhatourowntemplates+d2/graphvizemit.Schema:docs/design/drawing-card-schema.md.', NULL, '2026-06-2815:30:27', '2026-06-2815:30:27.974', '2026-06-2815:30:27.974', NULL, '936e0475a51e0881fb775116c75bbc14', 2) ON CONFLICT(hash) DO NOTHING;
INSERTINTOticket_history(ticket_record_id,field,old_value,new_value,changed_by,changed_at,created_at,updated_at,deleted_at,hash,canonical_version)VALUES('06FB2ERREMEEF26KKHGNZBWW64','description','Foundational build for the unified drawing card (epic T-317, decision D-91). A clide-owned canvas (CustomPaint) that renders from a JSON document: a PRIMITIVE scene-graph layer (rect/line/text/glyph/image at coordinates) plus a TEMPLATE-dispatch layer that maps a named component in the JSON to a predefined renderer. Templates lower onto the same primitive scene (hybrid model). Per drawn object, an optional label + description widget renders beneath it, only when those fields are present in the JSON. Display-only (D-78) — no inline selection. Driven via the clide CLI (D-6 parity), consuming the JSON input plumbing (T-315 / --file); mirror image.show''s Flutter-free handler + MessageBus publish + Claude-extension injection pattern. THIS ticket = the engine, the JSON schema, the primitive renderer, the dispatch mechanism, and the shared per-object label/description widget. Individual templates (image, icon, compare, svg, graph) are separate children. Acceptance: a JSON doc with raw primitives draws; a JSON doc naming a template dispatches to it; an object with label/description renders the caption widget; unknown template/primitive fails with a clear userError.
SCHEMADRAFTED(2026-06-28):docs/design/drawing-card-schema.md—declarativeJSONscene-graph(documentenvelope,primitivetypesrect/line/text/glyph/image,templateenvelope,sharedlabel/description,arbitrary-hexcolor,CLI''clidedraw--file'', error contract). Refined from the T-317 wireframe set.
MODELCLARIFICATION(user):webuildclide''sOWNnativeFlutterCustomPaintinterpretingadeclarativescene-graphdocument.WedoNOTporttheHTMLCanvas2DAPI—''HTMLcanvas''inD-91isonlythementalmodel(ageneraldrawingsurface,chosentorejectObsidian''s.canvasschema),notanAPItoimplement.ThedeclarativemodelisclosertoSVG/aretainedscene-graphthantocanvas''simperative2Dcontext.', 'Foundationalbuildfortheunifieddrawingcard(epicT-317,decisionD-91).Aclide-ownedcanvas(CustomPaint)thatrendersfromaJSONdocument:aPRIMITIVEscene-graphlayer(rect/line/text/glyph/imageatcoordinates)plusaTEMPLATE-dispatchlayerthatmapsanamedcomponentintheJSONtoapredefinedrenderer.Templateslowerontothesameprimitivescene(hybridmodel).Perdrawnobject,anoptionallabel+descriptionwidgetrendersbeneathit,onlywhenthosefieldsarepresentintheJSON.Display-only(D-78)—noinlineselection.DrivenviatheclideCLI(D-6parity),consumingtheJSONinputplumbing(T-315/--file); mirror image.show''s Flutter-free handler + MessageBus publish + Claude-extension injection pattern. THIS ticket = the engine, the JSON schema, the primitive renderer, the dispatch mechanism, and the shared per-object label/description widget. Individual templates (image, icon, compare, svg, graph) are separate children. Acceptance: a JSON doc with raw primitives draws; a JSON doc naming a template dispatches to it; an object with label/description renders the caption widget; unknown template/primitive fails with a clear userError.
SCHEMADRAFTED(2026-06-28):docs/design/drawing-card-schema.md—declarativeJSONscene-graph(documentenvelope,primitivetypesrect/line/text/glyph/image,templateenvelope,sharedlabel/description,arbitrary-hexcolor,CLI''clidedraw--file'', error contract). Refined from the T-317 wireframe set.
Mechanismshipped2026-06-28:''makedugite-check''(ci/check_dugite_version.sh)comparestheMakefileDUGITE_VERSIONpinagainstthelatestdesktop/dugite-nativereleaseandflagsCVE/securitymentionsinthenotes.D-59''scostline+aMakefilecommentnowpointatit(closestheclean-houseRULE-SUNSET-WITHOUT-TICKETflag).FIRSTCHECK(2026-06-28):bundledv2.53.0-3==latestv2.53.0-3—uptodate,noCVEflagged,nobumpneeded.RECURRING—donotclose.Nextcheckdue~2026-09-28(quarterly),orimmediatelyonagit/dugitesecurityadvisory(subscribe:github.com/git/git/security/advisories,github.com/desktop/dugite-native/security/advisories).Bump=D-63record+T-25(CImachine);''scheduled''CIcrondeferreduntilCIexists(T-25/T-491).', 'backlog', 'low', NULL, NULL, 'D-59', '2026-05-0614:34:35', '2026-06-2813:12:29.289', NULL, '0ef1067dc16303cce58e4b0fb485fa1f', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERTINTOtickets(record_id,type,parent_record_id,title,description,status,priority,assigned_to,team,decision_ref,created_at,updated_at,deleted_at,hash,canonical_version)VALUES('06FB2ACSDBDZARV3NNGYD9NYYR','task','06FB2EDCBYRBDSV9V1PJ1KE3CM','clide CLI: accept a JSON payload on stdin for structured commands','Add a stdin path to clide''s CLI so a command can receive a JSON payload piped in — `… | clide icon show --stdin`, `cat meta.json | clide image show foo.png --stdin` — instead of only positionals/flags or a `--file`.
##Why
Structuredcommands(thelabelledicon-cardentriesinT-313,imageannotationmetadatainT-316)wantaJSONpayloadthat''sawkwardtoexpressasflags.Todayclide''sCLIargvparser(lib/src/cli/argv_to_request.dart)onlyproducespositionals,--flags, and `-- passthrough`; there is no stdin path. T-313 therefore falls back to a `--file <path.json>` flag. A `--stdin` convention is the ergonomic peer of `--file` for piping, and is shared infra both icon.show and image.show consume.
Unblocksthepiped-JSONvariantsofT-313(iconentries)andT-316(imageannotations);bothcanalsoshipwith--file independently of this.
Deferred2026-06-28(user):blockedonitsconsumersT-316+T-313soitonlyresurfacesifoneactuallywantsthepipingUX.Standalone--stdin infra isn''t worth a build cycle now — both consumers can ship with --file (per this ticket), and structured JSON also flows natively through the MCP tool surface (D-86). Build it lazily inside whichever consumer first needs piping, if ever.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 10:52:34', '2026-06-28 13:28:49.359', NULL, 'cc06f073d3f02afba4ad728c364da0b8', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERTINTOtickets(record_id,type,parent_record_id,title,description,status,priority,assigned_to,team,decision_ref,created_at,updated_at,deleted_at,hash,canonical_version)VALUES('06FB234WP4Y6Q16A0HFW8BSXMG','task','06FB2EDCBYRBDSV9V1PJ1KE3CM','Native Phosphor glyph card — multi-icon list with optional labels/descriptions (previews + choice offers)','A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane — and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
-LABELLED/DESCRIBEDentries:a`--file <path.json>` flag whose value is a JSON array of `{"icon": "gear", "label": "Settings", "description": "global scope"}` (label, description optional); the handler reads and parses the file. NOTE: do NOT spec `--stdin` — clide''s CLI argv parser (lib/src/cli/argv_to_request.dart) only produces positionals/flags/passthrough and has no stdin path, so a `--file` flag (or repeated flags) is the grounded choice unless we deliberately add stdin support as separate work.
-RENDERPATH:validate+resolveiconnamesinthehandler(injectaresolverthewayimage.showinjectsImagePathResolver,soheadless/dart-teststaysfilesystem-free),thenpublishonadedicatedMessageBuschannel—e.g.`iconShowChannel=''icon''`,peerof`imageShowChannel=''image''`—capturedpost-bootinmain.dart;theClaudeextensionsubscribestothatliteralandinjectsthecardintotheprimarysession''sconversationlog.Honestfailure(IpcErroruserError/notFound)onanunknownglyphnameoramalformed/missing--file, and on no live UI bus (headless), mirroring image.show.
-A`--file <path.json>` payload lets each icon carry an optional label and optional description, both rendered alongside the glyph (no --stdin — not supported by the CLI parser).
FOLLOW-UPS:thepiped-JSON(--stdin) variant is split out as T-315 (generic CLI stdin plumbing); image.show gets the same metadata/annotation treatment in T-316. T-313 ships with --file regardless of T-315.
DESIGNREFINEMENT(2026-06-28,wireframereview):broadentheper-entrysize-samplestripbeyondclide''sinlinetypographytokens.clideiconshowisageneral-purposeglyphpreviewer,notboundtoclide''sownUI,sothestriprendersBOTHclideinlinetokens(11–15:badge/small/meta/caption/body)ANDcommoniconsizes(18,20,24,32,48).Herostays52(clideFontWelcomeBanner).Updatetheacceptancebulletaccordingly:''asampleateachinlinetoken(11–15)PLUScommonsizes18/20/24/32/48'',drivenoffnamedtokenswheretheyexistandexplicitpxotherwise.Wireframe:docs/design/wireframes/conversation/icon-glyph-card.png.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-1010:20:54', '2026-06-2813:43:39.648', NULL, '1bd3db3360aa4145ca351b10a829141f', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERTINTOtickets(record_id,type,parent_record_id,title,description,status,priority,assigned_to,team,decision_ref,created_at,updated_at,deleted_at,hash,canonical_version)VALUES('06FB234WP4Y6Q16A0HFW8BSXMG','task','06FB2EDCBYRBDSV9V1PJ1KE3CM','Native Phosphor glyph card — multi-icon list with optional labels/descriptions (previews + choice offers)','A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane — and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
-LABELLED/DESCRIBEDentries:a`--file <path.json>` flag whose value is a JSON array of `{"icon": "gear", "label": "Settings", "description": "global scope"}` (label, description optional); the handler reads and parses the file. NOTE: do NOT spec `--stdin` — clide''s CLI argv parser (lib/src/cli/argv_to_request.dart) only produces positionals/flags/passthrough and has no stdin path, so a `--file` flag (or repeated flags) is the grounded choice unless we deliberately add stdin support as separate work.
-RENDERPATH:validate+resolveiconnamesinthehandler(injectaresolverthewayimage.showinjectsImagePathResolver,soheadless/dart-teststaysfilesystem-free),thenpublishonadedicatedMessageBuschannel—e.g.`iconShowChannel=''icon''`,peerof`imageShowChannel=''image''`—capturedpost-bootinmain.dart;theClaudeextensionsubscribestothatliteralandinjectsthecardintotheprimarysession''sconversationlog.Honestfailure(IpcErroruserError/notFound)onanunknownglyphnameoramalformed/missing--file, and on no live UI bus (headless), mirroring image.show.
-A`--file <path.json>` payload lets each icon carry an optional label and optional description, both rendered alongside the glyph (no --stdin — not supported by the CLI parser).
FOLLOW-UPS:thepiped-JSON(--stdin) variant is split out as T-315 (generic CLI stdin plumbing); image.show gets the same metadata/annotation treatment in T-316. T-313 ships with --file regardless of T-315.
SizesetFINALIZED(2026-06-28):onecontinuoussamplestrip—10,11,12,13,14,15,18,20,24,32,48px—noclide-vs-commonvisualsplit.Herostays52.Supersedesthegroupedframinginthepriornote.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-1010:20:54', '2026-06-2813:47:44.006', NULL, '8bb45b8217291cc6d2f974896fcc4a27', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERTINTOtickets(record_id,type,parent_record_id,title,description,status,priority,assigned_to,team,decision_ref,created_at,updated_at,deleted_at,hash,canonical_version)VALUES('06FB234WP4Y6Q16A0HFW8BSXMG','task','06FB2EDCBYRBDSV9V1PJ1KE3CM','Native Phosphor glyph card — multi-icon list with optional labels/descriptions (previews + choice offers)','A conversation-pane card that renders one OR MANY Phosphor glyphs by name/codepoint, each with an optional label and an optional description line, so icons can be previewed and compared in the live pane — and so a set of candidate icons can be offered as a labelled choice list (e.g. picking settings-scope icons, T-302).
-LABELLED/DESCRIBEDentries:a`--file <path.json>` flag whose value is a JSON array of `{"icon": "gear", "label": "Settings", "description": "global scope"}` (label, description optional); the handler reads and parses the file. NOTE: do NOT spec `--stdin` — clide''s CLI argv parser (lib/src/cli/argv_to_request.dart) only produces positionals/flags/passthrough and has no stdin path, so a `--file` flag (or repeated flags) is the grounded choice unless we deliberately add stdin support as separate work.
-RENDERPATH:validate+resolveiconnamesinthehandler(injectaresolverthewayimage.showinjectsImagePathResolver,soheadless/dart-teststaysfilesystem-free),thenpublishonadedicatedMessageBuschannel—e.g.`iconShowChannel=''icon''`,peerof`imageShowChannel=''image''`—capturedpost-bootinmain.dart;theClaudeextensionsubscribestothatliteralandinjectsthecardintotheprimarysession''sconversationlog.Honestfailure(IpcErroruserError/notFound)onanunknownglyphnameoramalformed/missing--file, and on no live UI bus (headless), mirroring image.show.
-A`--file <path.json>` payload lets each icon carry an optional label and optional description, both rendered alongside the glyph (no --stdin — not supported by the CLI parser).
FOLLOW-UPS:thepiped-JSON(--stdin) variant is split out as T-315 (generic CLI stdin plumbing); image.show gets the same metadata/annotation treatment in T-316. T-313 ships with --file regardless of T-315.
COLOR(2026-06-28,foldedintopayloadspec):addanOPTIONALper-entry''color''tothe--file JSON, e.g. {"icon":"gear","label":"Settings","color":"#e2b714"}. Value is an ARBITRARY color — hex #rrggbb / #rrggbbaa (CSS-style names acceptable) — passed straight to PhosphorIconPainter. NOT a clide SurfaceTokens theme token: the glyph is content for whatever project we''re working on (its own palette), and the D-7 token discipline governs clide''s own chrome, not rendered content. Optional card-level default ''color''; a glyph with none falls back to the card''s default foreground. Unknown/malformed color → the same honest userError as a bad glyph name. Mirrors how arbitrary colors already flow through the image/svg cards.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-10 10:20:54', '2026-06-28 14:00:47.466', NULL, 'ed52cbbebf02660ef217c7f948ca66db', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERTINTOtickets(record_id,type,parent_record_id,title,description,status,priority,assigned_to,team,decision_ref,created_at,updated_at,deleted_at,hash,canonical_version)VALUES('06FGX26B0NAC9WRJMVB0QE6CV8','task','06FB2EDCBYRBDSV9V1PJ1KE3CM','D2 diagram card — compile d2 source to SVG, reuse the SVG render widget',NULL,'backlog','medium',NULL,NULL,NULL,'2026-06-28 14:01:13.733','2026-06-28 14:01:13.733',NULL,'a5abd32f6dbe24a6e10ad52edfa568d7',2)ONCONFLICT(record_id)DOUPDATESETtype=excluded.type,parent_record_id=excluded.parent_record_id,title=excluded.title,description=excluded.description,status=excluded.status,priority=excluded.priority,assigned_to=excluded.assigned_to,team=excluded.team,decision_ref=excluded.decision_ref,updated_at=excluded.updated_at,deleted_at=excluded.deleted_at,hash=excluded.hash,canonical_version=excluded.canonical_versionWHEREexcluded.updated_at>tickets.updated_atOR(excluded.updated_at=tickets.updated_atANDexcluded.hash>tickets.hash);
INSERTINTOtickets(record_id,type,parent_record_id,title,description,status,priority,assigned_to,team,decision_ref,created_at,updated_at,deleted_at,hash,canonical_version)VALUES('06FGX26B0NAC9WRJMVB0QE6CV8','task','06FB2EDCBYRBDSV9V1PJ1KE3CM','D2 diagram card — compile d2 source to SVG, reuse the SVG render widget','A conversation-pane drawing card (peer of the SVG card T-320) that renders a D2 diagram. SEPARATE card type from SVG, but the ONLY difference is a compile step in front: D2 source → compile to SVG → render through the SAME SVG widget (T-320). Blocked on T-320 for that reason.
CLI/parity(D-6):drivenviathedrawing-carddispatch(T-318)—''clidedraw--file pipeline.d2'' (type inferred from .d2), or template:"d2" with inline source in the drawing-card JSON. Honest userError on a d2 compile failure (bad source), mirroring image/svg validation.
Open:D2compilerdependency—d2isGo(thereisad2-diagramskill).Decidewhethertoshellouttoad2binary(supporter-toolpattern,peerofpql)orvendorit;flagbeforebuilding.Wireframe:docs/design/wireframes/conversation/d2-render-card.png.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-2814:01:13.733', '2026-06-2814:01:24.709', NULL, 'c51387a44d70061be160ab331513db27', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERTINTOtickets(record_id,type,parent_record_id,title,description,status,priority,assigned_to,team,decision_ref,created_at,updated_at,deleted_at,hash,canonical_version)VALUES('06FB2ERREMEEF26KKHGNZBWW64','story','06FB2EDCBYRBDSV9V1PJ1KE3CM','Drawing card core: canvas engine + JSON schema + template dispatch','Foundational build for the unified drawing card (epic T-317, decision D-91). A clide-owned canvas (CustomPaint) that renders from a JSON document: a PRIMITIVE scene-graph layer (rect/line/text/glyph/image at coordinates) plus a TEMPLATE-dispatch layer that maps a named component in the JSON to a predefined renderer. Templates lower onto the same primitive scene (hybrid model). Per drawn object, an optional label + description widget renders beneath it, only when those fields are present in the JSON. Display-only (D-78) — no inline selection. Driven via the clide CLI (D-6 parity), consuming the JSON input plumbing (T-315 / --file); mirror image.show''s Flutter-free handler + MessageBus publish + Claude-extension injection pattern. THIS ticket = the engine, the JSON schema, the primitive renderer, the dispatch mechanism, and the shared per-object label/description widget. Individual templates (image, icon, compare, svg, graph) are separate children. Acceptance: a JSON doc with raw primitives draws; a JSON doc naming a template dispatches to it; an object with label/description renders the caption widget; unknown template/primitive fails with a clear userError.
SCHEMADRAFTED(2026-06-28):docs/design/drawing-card-schema.md—declarativeJSONscene-graph(documentenvelope,primitivetypesrect/line/text/glyph/image,templateenvelope,sharedlabel/description,arbitrary-hexcolor,CLI''clidedraw--file'', error contract). Refined from the T-317 wireframe set.
MODELCLARIFICATION(user):webuildclide''sOWNnativeFlutterCustomPaintinterpretingadeclarativescene-graphdocument.WedoNOTporttheHTMLCanvas2DAPI—''HTMLcanvas''inD-91isonlythementalmodel(ageneraldrawingsurface,chosentorejectObsidian''s.canvasschema),notanAPItoimplement.ThedeclarativemodelisclosertoSVG/aretainedscene-graphthantocanvas''simperative2Dcontext.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-1011:11:40', '2026-06-2815:20:03.628', NULL, '70c4bf1a4c0c8917d4f7830642bb1713', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERTINTOtickets(record_id,type,parent_record_id,title,description,status,priority,assigned_to,team,decision_ref,created_at,updated_at,deleted_at,hash,canonical_version)VALUES('06FB2ETJQP0CT6X7W3CWZ6NS9G','task','06FB2EDCBYRBDSV9V1PJ1KE3CM','Drawing card template: SVG render','Template for the unified drawing card (T-317, D-91): render an SVG file referenced in the JSON. clide-owned rendering per the own-the-rendering-stack guardrail — no opinionated SVG package without justification; evaluate what is feasible via CustomPaint / a minimal path parser, and scope accordingly (SVG is a broad spec; define the supported subset). Optional label + description beneath (T-318). Display-only (D-78). Depends on the core engine (T-318).
PROMOTEDTOENGINE(D-103,2026-06-28):thisisnowthedrawing-card''srenderingFOUNDATION,notonetemplateamongmany.Buildtheclide-ownedCustomPaintSVGrendereroveraBOUNDEDsubset(rect/line/polyline/polygon/circle/ellipse/path,textincl.thePhosphorfont,image,g+transform,basicfill/stroke/opacity/rx).T-318(envelope+dispatch+Flutteroverlay)andeverytemplatelowerontothis;d2/graphviz/mermaidoutputrendersforfree.NolongerblockedbyT-318(dependencyinverted).Open:pinthesubsettowhatourowntemplates+d2/graphvizemit.Schema:docs/design/drawing-card-schema.md.', 'backlog', 'low', NULL, NULL, NULL, '2026-06-1011:11:55', '2026-06-2815:30:27.974', NULL, '31922dc79d19a564325ab3e30bd2927b', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERTINTOtickets(record_id,type,parent_record_id,title,description,status,priority,assigned_to,team,decision_ref,created_at,updated_at,deleted_at,hash,canonical_version)VALUES('06FB2ERREMEEF26KKHGNZBWW64','story','06FB2EDCBYRBDSV9V1PJ1KE3CM','Drawing card core: canvas engine + JSON schema + template dispatch','Foundational build for the unified drawing card (epic T-317, decision D-91). A clide-owned canvas (CustomPaint) that renders from a JSON document: a PRIMITIVE scene-graph layer (rect/line/text/glyph/image at coordinates) plus a TEMPLATE-dispatch layer that maps a named component in the JSON to a predefined renderer. Templates lower onto the same primitive scene (hybrid model). Per drawn object, an optional label + description widget renders beneath it, only when those fields are present in the JSON. Display-only (D-78) — no inline selection. Driven via the clide CLI (D-6 parity), consuming the JSON input plumbing (T-315 / --file); mirror image.show''s Flutter-free handler + MessageBus publish + Claude-extension injection pattern. THIS ticket = the engine, the JSON schema, the primitive renderer, the dispatch mechanism, and the shared per-object label/description widget. Individual templates (image, icon, compare, svg, graph) are separate children. Acceptance: a JSON doc with raw primitives draws; a JSON doc naming a template dispatches to it; an object with label/description renders the caption widget; unknown template/primitive fails with a clear userError.
SCHEMADRAFTED(2026-06-28):docs/design/drawing-card-schema.md—declarativeJSONscene-graph(documentenvelope,primitivetypesrect/line/text/glyph/image,templateenvelope,sharedlabel/description,arbitrary-hexcolor,CLI''clidedraw--file'', error contract). Refined from the T-317 wireframe set.
- [D-103: Drawing-card primitive layer is SVG; the SVG renderer is the engine](decisions/architecture.md#d-103-drawing-card-primitive-layer-is-svg-the-svg-renderer-is-the-engine) — _architecture_
- **Cross-reference:** Fonts landed it: T-460 (Inter default + UI picker) and T-471 (mono picker) migrated ~93 sites onto `ClideSettings.fonts`. Consumer migration of theme + i18n onto the facade, and the context-less font stragglers (T-472), are staged follow-ups. Values live in the kernel `SettingsStore`.
- **Raised by:** 2026-06-17 — user, during T-471 font-flow design: "I do see reason in nesting them all in one settings object that dynamically loads so we can extend it in the future… plumb once, use many."
### D-103: Drawing-card primitive layer is SVG; the SVG renderer is the engine
- **Date:** 2026-06-28
- **Decision:** The unified drawing card's primitive / scene-graph layer is **SVG**, not a bespoke `{type:"rect",…}` JSON vocabulary. The clide-owned `CustomPaint` SVG renderer (T-320) **is** the card's rendering engine — the foundation the rest builds on, not one template among many. The low-level escape hatch is "send SVG" (`template:"svg"` with inline `source`, or a `.svg` path); the high-level **templates** (image / icon / compare / d2) stay as JSON sugar but **lower to SVG** rather than to a custom primitive scene. A thin **Flutter overlay** renders the clide chrome that is *not* content — the per-object label/description caption widgets and the lightbox affordance — layered over the rendered SVG, anchored to elements via `data-label` / `data-description` / `data-lightbox` attributes. The **graph template is the exception:** it embeds the interactive native graph subsystem ([D-46], T-323) as a live widget rather than lowering to static SVG. Driven via `clide draw --file` (D-6); Flutter-free handler → `draw` MessageBus channel → Claude-extension injection.
- **Rationale:** SVG already *is* a declarative, standard scene-graph with precisely the primitives [D-91] wanted (rect/line/text/image/path at coordinates, transforms). Since a clide-owned SVG renderer is being built for the `svg` template regardless, inventing a parallel primitive JSON + a second renderer duplicates the work for one job. Making SVG the substrate collapses two renderers and two schemas into one, makes external SVG / graphviz / mermaid / d2 output renderable for free, and keeps the `.canvas`-viewer reuse ([D-91]) on the same path. Owning the SVG painter (`CustomPaint`, no package) honors "own the rendering stack" — SVG here is a document *format*, not a third-party renderer, and explicitly **not** the imperative HTML Canvas 2D API ("HTML `<canvas>`" in D-91 was a mental model to reject Obsidian's `.canvas` schema, never an API to port).
- **Cost:** The SVG-subset scope becomes the card's central design surface (the substrate, not one template) — but bounded, since the only SVG that must render is what clide's own templates + d2/graphviz emit; clide controls both ends. Re-sequences epic T-317: the SVG renderer (T-320) lands **before** the core envelope (T-318), inverting the prior `T-320 → T-318` dependency. The card is a hybrid (SVG content + Flutter overlay), so captions/interaction are not expressible in the document SVG itself.
- **Amends [D-91]:** D-91's "raw primitives (rects/lines/text at coordinates)" are now SVG elements; "templates lower onto the same primitive scene" becomes "templates lower to SVG"; the renderer foundation is the SVG painter (T-320), not a separate primitive engine inside T-318 (which becomes the document envelope + template dispatch + the Flutter overlay).
- **Raised by:** 2026-06-28 — user, during the drawing-card schema draft: "if we are close to svg, are we not better off extending svg instead?" Confirmed the primitive layer should be SVG with templates lowering to it, a Flutter overlay for captions/interaction, and graph as the live-widget exception.
---
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.