Skip to main content

tern_sdk/ui/
mod.rs

1//! Layer 3: nodes. Typed builders for every kind (except `block`, which is
2//! Tern's own), every documented prop, the common props, styled spans and
3//! handlers that map events to the program's messages.
4//!
5//! Every builder converts into [`Node`]; containers take children with
6//! `.child(…)` / `.children(…)`. Any prop can also be set untyped with
7//! `.prop(name, value)`, and any kind built with [`node`].
8//!
9//! # Example
10//! ```
11//! use tern_sdk::ui::{self, CardStatus, View};
12//!
13//! #[derive(Clone)]
14//! enum Msg {
15//! 	Rerun,
16//! }
17//!
18//! let view: View<Msg> = View::new().main([ui::card()
19//! 	.head("Deploy")
20//! 	.status(CardStatus::Running)
21//! 	.on_menu("rerun", Msg::Rerun)
22//! 	.child(ui::md("Uploading **api-gateway**"))
23//! 	.child(ui::progress().value(0.4).label("40%"))]);
24//! assert!(view.regions()[0].is_some());
25//! ```
26
27mod data;
28pub mod html;
29mod icon;
30pub(crate) mod node;
31mod types;
32
33use std::time::Instant;
34
35pub use data::*;
36pub use icon::Icon;
37pub use node::{Node, View};
38pub use types::*;
39
40use self::node::{
41	actions_methods, base_methods, children_methods, event_methods, kind_type, max_method, props,
42	title_method,
43};
44
45/// The status chip of a card (the same values as tools).
46pub type CardStatus = Status;
47
48/// Builds `Vec<Span>` from strings and spans.
49///
50/// # Example
51/// ```
52/// use tern_sdk::{spans, ui};
53///
54/// let label = spans!["Waiting for ", ui::span("CI").style("strong")];
55/// assert_eq!(label.len(), 2);
56/// ```
57#[macro_export]
58macro_rules! spans {
59	($($s:expr),* $(,)?) => {
60		vec![$($crate::ui::Span::from($s)),*]
61	};
62}
63
64/// Builds `Vec<Node<M>>` from builders of different kinds.
65///
66/// # Example
67/// ```
68/// use tern_sdk::{nodes, ui, ui::View};
69///
70/// let view: View = View::new().main(nodes![ui::md("# Hi"), ui::rule(), ui::badge("new")]);
71/// assert_eq!(view.regions()[0].unwrap().child_nodes().len(), 3);
72/// ```
73#[macro_export]
74macro_rules! nodes {
75	($($n:expr),* $(,)?) => {
76		vec![$($crate::ui::Node::from($n)),*]
77	};
78}
79
80/// A styled span of text.
81pub fn span(t: impl Into<String>) -> Span {
82	Span::new(t)
83}
84
85/// A node of any kind (the untyped escape hatch).
86pub fn node<M>(kind: impl Into<String>) -> Node<M> {
87	Node::new(kind)
88}
89
90/// Defines a kind's builder with the common props.
91macro_rules! common_kind {
92	($(#[$m:meta])* $name:ident) => {
93		kind_type!($(#[$m])* $name);
94
95		impl<M> $name<M> {
96			base_methods!();
97			title_method!();
98			max_method!();
99			actions_methods!();
100			event_methods!();
101		}
102	};
103}
104
105/// Defines a container kind's builder with the common props.
106macro_rules! container_kind {
107	($(#[$m:meta])* $name:ident) => {
108		common_kind!($(#[$m])* $name);
109
110		impl<M> $name<M> {
111			children_methods!();
112		}
113	};
114}
115
116/// A new builder of `kind`.
117fn new<M, T: From<Node<M>>>(kind: &str) -> T {
118	T::from(Node::new(kind))
119}
120
121/// Implements `From<Node>` for builder types (used by [`new`]).
122macro_rules! from_node {
123	($($name:ident),*) => {
124		$(impl<M> From<Node<M>> for $name<M> {
125			fn from(node: Node<M>) -> Self {
126				Self(node)
127			}
128		})*
129	};
130}
131
132from_node!(
133	Col, Row, Card, Section, Rule, Spacer, TextNode, Md, Code, Diff, Ansi, Rows, Math, Kv, Table,
134	Tree, Badge, Kbd, IconNode, Image, List, Item, Tabs, Picker, Spinner, Shimmer, Elapsed, Rate,
135	Progress, Meter, Chart, Effort, Editor, Input, StatusBar, Seg, Toast, Overlay, Tool, Agent,
136	Checklist, Prefs
137);
138
139// ---------------------------------------------------------------------------
140// Layout
141// ---------------------------------------------------------------------------
142
143container_kind!(
144	/// `col`: a vertical stack.
145	Col
146);
147
148impl<M> Col<M> {
149	props! {
150		/// Space between children.
151		gap(Gap) = "gap";
152		/// Cross-axis alignment.
153		align(Align) = "align";
154		/// Main-axis distribution.
155		justify(Justify) = "justify";
156		/// Adds class `wrap`.
157		wrap(bool) = "wrap";
158	}
159}
160
161/// A vertical stack.
162pub fn col<M>() -> Col<M> {
163	new("col")
164}
165
166container_kind!(
167	/// `row`: a horizontal flex row.
168	Row
169);
170
171impl<M> Row<M> {
172	props! {
173		/// Space between children.
174		gap(Gap) = "gap";
175		/// Cross-axis alignment.
176		align(Align) = "align";
177		/// Main-axis distribution.
178		justify(Justify) = "justify";
179		/// Children flow onto more lines when they don't fit.
180		wrap(bool) = "wrap";
181	}
182}
183
184/// A horizontal flex row.
185pub fn row<M>() -> Row<M> {
186	new("row")
187}
188
189container_kind!(
190	/// `card`: a rounded card with a head and a body, optionally folding.
191	Card
192);
193
194impl<M> Card<M> {
195	props! {
196		/// Head content: text, spans, or a child's id.
197		head(Text) = "head";
198		/// The status chip.
199		status(Status) = "status";
200		/// The head flips the fold.
201		collapsible(bool) = "collapsible";
202		/// The starting fold state.
203		collapsed(bool) = "collapsed";
204		/// Lines shown while collapsed.
205		preview(Preview) = "preview";
206		/// An accent ring and halo.
207		selected(bool) = "selected";
208		/// The body has no padding.
209		inset(bool) = "inset";
210		/// The bare variant.
211		variant(CardVariant) = "variant";
212	}
213}
214
215/// A card.
216pub fn card<M>() -> Card<M> {
217	new("card")
218}
219
220container_kind!(
221	/// `section`: a lighter disclosure group.
222	Section
223);
224
225impl<M> Section<M> {
226	props! {
227		/// Head content.
228		head(Text) = "head";
229		/// The head flips the fold.
230		collapsible(bool) = "collapsible";
231		/// The starting fold state.
232		collapsed(bool) = "collapsed";
233	}
234}
235
236/// A section.
237pub fn section<M>() -> Section<M> {
238	new("section")
239}
240
241common_kind!(
242	/// `rule`: a divider hairline.
243	Rule
244);
245
246impl<M> Rule<M> {
247	props! {
248		/// A centered label.
249		label(Text) = "label";
250	}
251}
252
253/// A divider.
254pub fn rule<M>() -> Rule<M> {
255	new("rule")
256}
257
258common_kind!(
259	/// `spacer`: vertical space.
260	Spacer
261);
262
263impl<M> Spacer<M> {
264	props! {
265		/// The height.
266		size(Gap) = "size";
267	}
268}
269
270/// Vertical space.
271pub fn spacer<M>() -> Spacer<M> {
272	new("spacer")
273}
274
275// ---------------------------------------------------------------------------
276// Text and code
277// ---------------------------------------------------------------------------
278
279common_kind!(
280	/// `text`: styled spans, wrapped or truncated.
281	TextNode
282);
283
284impl<M> TextNode<M> {
285	props! {
286		/// How it wraps.
287		wrap(Wrap) = "wrap";
288		/// Where it is cut.
289		truncate(Truncate) = "truncate";
290		/// Clamp to this many lines.
291		lines(u32) = "lines";
292		/// Caps the line length.
293		measure(Measure) = "measure";
294	}
295
296	/// Replaces the text (plain or spans).
297	pub fn text(mut self, text: impl Into<Text>) -> Self {
298		self.0.set_text(text.into());
299		self
300	}
301}
302
303/// Text, plain or styled.
304pub fn text<M>(text: impl Into<Text>) -> TextNode<M> {
305	let mut node = Node::new("text");
306	node.set_text(text.into());
307	TextNode(node)
308}
309
310common_kind!(
311	/// `md`: Markdown.
312	Md
313);
314
315impl<M> Md<M> {
316	props! {
317		/// The Markdown source.
318		text(String) = "text";
319		/// The source is still arriving.
320		stream(bool) = "stream";
321		/// Spans drawn in place of their literal text.
322		marks(Vec<Span>) = "marks";
323	}
324}
325
326/// Markdown.
327pub fn md<M>(source: impl Into<String>) -> Md<M> {
328	new::<M, Md<M>>("md").text(source)
329}
330
331common_kind!(
332	/// `code`: highlighted code.
333	Code
334);
335
336impl<M> Code<M> {
337	props! {
338		/// The code.
339		text(String) = "text";
340		/// The grammar by language name or extension.
341		lang(String) = "lang";
342		/// A file path: picks the grammar and shows a header.
343		path(String) = "path";
344		/// Show line numbers.
345		numbers(bool) = "numbers";
346		/// The first line's number.
347		start(i64) = "start";
348		/// Marked lines.
349		marks(Vec<CodeMark>) = "marks";
350		/// Soft-wrap long lines.
351		wrap(bool) = "wrap";
352	}
353}
354
355/// Highlighted code.
356pub fn code<M>(source: impl Into<String>) -> Code<M> {
357	new::<M, Code<M>>("code").text(source)
358}
359
360common_kind!(
361	/// `diff`: a unified or split diff.
362	Diff
363);
364
365impl<M> Diff<M> {
366	props! {
367		/// A unified diff.
368		text(String) = "text";
369		/// Hunks given directly.
370		hunks(Vec<Hunk>) = "hunks";
371		/// Picks the grammar.
372		path(String) = "path";
373		/// The grammar by language name.
374		lang(String) = "lang";
375		/// How it lays out.
376		mode(DiffMode) = "mode";
377	}
378}
379
380/// A diff.
381pub fn diff<M>() -> Diff<M> {
382	new("diff")
383}
384
385common_kind!(
386	/// `ansi`: output with escape sequences.
387	Ansi
388);
389
390impl<M> Ansi<M> {
391	props! {
392		/// The output.
393		text(String) = "text";
394		/// Wrap at this many columns.
395		cols(u32) = "cols";
396		/// Keep the tail in view.
397		follow(bool) = "follow";
398	}
399
400	/// Clamps to `lines` lines under a fade with a "more lines" button.
401	pub fn preview(mut self, lines: u32) -> Self {
402		self
403			.0
404			.set("preview", &serde_json::json!({ "lines": lines }));
405		self
406	}
407}
408
409/// Terminal output.
410pub fn ansi<M>(output: impl Into<String>) -> Ansi<M> {
411	new::<M, Ansi<M>>("ansi").text(output)
412}
413
414common_kind!(
415	/// `rows`: pre-rendered ANSI rows (the migration fallback).
416	Rows
417);
418
419impl<M> Rows<M> {
420	props! {
421		/// The rows.
422		lines(Vec<String>) = "lines";
423		/// The exact width in columns.
424		cols(u32) = "cols";
425	}
426}
427
428/// Pre-rendered rows.
429pub fn rows<M>(lines: impl Into<Vec<String>>) -> Rows<M> {
430	new::<M, Rows<M>>("rows").lines(lines)
431}
432
433common_kind!(
434	/// `math`: TeX math.
435	Math
436);
437
438impl<M> Math<M> {
439	props! {
440		/// The TeX, without delimiters.
441		text(String) = "text";
442		/// Display style.
443		display(bool) = "display";
444	}
445}
446
447/// TeX math.
448pub fn math<M>(tex: impl Into<String>) -> Math<M> {
449	new::<M, Math<M>>("math").text(tex)
450}
451
452// ---------------------------------------------------------------------------
453// Data
454// ---------------------------------------------------------------------------
455
456common_kind!(
457	/// `kv`: an aligned key/value list.
458	Kv
459);
460
461impl<M> Kv<M> {
462	props! {
463		/// All pairs.
464		items(Vec<KvItem>) = "items";
465		/// How it lays out.
466		layout(KvLayout) = "layout";
467	}
468
469	/// Adds a pair.
470	pub fn item(mut self, k: impl Into<Text>, v: impl Into<Text>) -> Self {
471		push(&mut self.0, "items", &KvItem { k: k.into(), v: v.into() });
472		self
473	}
474}
475
476/// A key/value list.
477pub fn kv<M>() -> Kv<M> {
478	new("kv")
479}
480
481common_kind!(
482	/// `table`: a table with priority-hidden columns and meter cells.
483	Table
484);
485
486impl<M> Table<M> {
487	props! {
488		/// All columns.
489		cols(Vec<Column>) = "cols";
490		/// All rows.
491		rows(Vec<TableRow>) = "rows";
492	}
493
494	/// Adds a column.
495	pub fn col(mut self, col: Column) -> Self {
496		push(&mut self.0, "cols", &col);
497		self
498	}
499
500	/// Adds a row.
501	pub fn row(mut self, row: TableRow) -> Self {
502		push(&mut self.0, "rows", &row);
503		self
504	}
505}
506
507/// A table.
508pub fn table<M>() -> Table<M> {
509	new("table")
510}
511
512common_kind!(
513	/// `tree`: a disclosure tree.
514	Tree
515);
516
517impl<M> Tree<M> {
518	props! {
519		/// The top-level items.
520		nodes(Vec<TreeItem>) = "nodes";
521	}
522
523	/// Adds a top-level item.
524	pub fn item(mut self, item: TreeItem) -> Self {
525		push(&mut self.0, "nodes", &item);
526		self
527	}
528}
529
530/// A disclosure tree.
531pub fn tree<M>() -> Tree<M> {
532	new("tree")
533}
534
535common_kind!(
536	/// `badge`: a pill chip.
537	Badge
538);
539
540impl<M> Badge<M> {
541	props! {
542		/// The label (plain text).
543		text(String) = "text";
544	}
545}
546
547/// A pill chip.
548pub fn badge<M>(text: impl Into<String>) -> Badge<M> {
549	new::<M, Badge<M>>("badge").text(text)
550}
551
552common_kind!(
553	/// `kbd`: keycaps.
554	Kbd
555);
556
557impl<M> Kbd<M> {
558	props! {
559		/// Key names, in order.
560		keys(Vec<String>) = "keys";
561	}
562}
563
564/// Keycaps (`["cmd", "K"]`).
565pub fn kbd<M, K: Into<String>>(keys: impl IntoIterator<Item = K>) -> Kbd<M> {
566	new::<M, Kbd<M>>("kbd").keys(keys.into_iter().map(Into::into).collect::<Vec<_>>())
567}
568
569common_kind!(
570	/// `icon`: a named icon.
571	IconNode
572);
573
574impl<M> IconNode<M> {
575	props! {
576		/// The icon.
577		name(Icon) = "name";
578	}
579}
580
581/// A named icon.
582pub fn icon<M>(name: impl Into<Icon>) -> IconNode<M> {
583	new::<M, IconNode<M>>("icon").name(name)
584}
585
586common_kind!(
587	/// `image`: an image from a blob.
588	Image
589);
590
591impl<M> Image<M> {
592	props! {
593		/// A blob id.
594		blob(String) = "blob";
595		/// An image Tern ships (`"omp"`).
596		builtin(String) = "builtin";
597		/// The accessible name.
598		alt(String) = "alt";
599		/// Natural width in px.
600		w(f64) = "w";
601		/// Natural height in px.
602		h(f64) = "h";
603		/// A file the zoom viewer may open.
604		path(String) = "path";
605	}
606}
607
608/// An image.
609pub fn image<M>() -> Image<M> {
610	new("image")
611}
612
613// ---------------------------------------------------------------------------
614// Lists, tabs and pickers
615// ---------------------------------------------------------------------------
616
617kind_type!(
618	/// `list`: a selectable list of `item`s.
619	List
620);
621
622impl<M> List<M> {
623	base_methods!();
624
625	title_method!();
626
627	actions_methods!();
628
629	event_methods!();
630
631	children_methods!();
632
633	props! {
634		/// The selected item's id.
635		selected(String) = "selected";
636		/// A query marked in each item's label.
637		filter(String) = "filter";
638		/// The line shown when empty.
639		empty(Text) = "empty";
640		/// Virtualize the rows.
641		virtualize(bool) = "virtual";
642	}
643
644	/// Caps the height at `lines` rows (the list scrolls itself).
645	pub fn max_lines(mut self, lines: u32) -> Self {
646		self.0.set("max", &serde_json::json!({ "lines": lines }));
647		self
648	}
649
650	/// Caps the height at a fraction of the window's.
651	pub fn max_fraction(mut self, fraction: f64) -> Self {
652		self.0.set("max", &fraction);
653		self
654	}
655}
656
657/// A selectable list.
658pub fn list<M>() -> List<M> {
659	new("list")
660}
661
662common_kind!(
663	/// `item`: one list row.
664	Item
665);
666
667impl<M> Item<M> {
668	props! {
669		/// The row's text.
670		label(Text) = "label";
671		/// Dim text after the label.
672		detail(Text) = "detail";
673		/// Right-aligned text.
674		value(Text) = "value";
675		/// An icon.
676		icon(Icon) = "icon";
677		/// Key names drawn as keycaps.
678		hint(Vec<String>) = "hint";
679		/// Dims the row; it sends nothing.
680		disabled(bool) = "disabled";
681	}
682}
683
684/// A list row.
685pub fn item<M>(label: impl Into<Text>) -> Item<M> {
686	new::<M, Item<M>>("item").label(label)
687}
688
689common_kind!(
690	/// `tabs`: a tab strip.
691	Tabs
692);
693
694impl<M> Tabs<M> {
695	props! {
696		/// All tabs.
697		items(Vec<Tab>) = "items";
698		/// The active tab's id.
699		active(String) = "active";
700	}
701
702	/// Adds a tab.
703	pub fn tab(mut self, id: impl Into<String>, label: impl Into<Text>) -> Self {
704		push(&mut self.0, "items", &Tab { id: id.into(), label: label.into() });
705		self
706	}
707}
708
709/// A tab strip.
710pub fn tabs<M>() -> Tabs<M> {
711	new("tabs")
712}
713
714kind_type!(
715	/// `picker`: a searchable picker sheet (its child is the preview).
716	Picker
717);
718
719impl<M> Picker<M> {
720	base_methods!();
721
722	max_method!();
723
724	event_methods!();
725
726	children_methods!();
727
728	props! {
729		/// The sheet's size.
730		size(PickerSize) = "size";
731		/// The row template.
732		layout(PickerLayout) = "layout";
733		/// Where the preview sits.
734		preview(PickerPreview) = "preview";
735		/// The heading.
736		title(Text) = "title";
737		/// The heading's second part.
738		subtitle(Text) = "subtitle";
739		/// The head icon.
740		icon(Icon) = "icon";
741		/// The search text (unset hides the search).
742		query(String) = "query";
743		/// The caret in `query`, UTF-16.
744		cursor(usize) = "cursor";
745		/// The search placeholder.
746		placeholder(String) = "placeholder";
747		/// What the rows are.
748		noun(String) = "noun";
749		/// Which part the keys drive.
750		focus(PickerFocus) = "focus";
751		/// Ready, loading or error.
752		state(PickerState) = "state";
753		/// The error card's text.
754		message(Text) = "message";
755		/// The text when there are no rows.
756		empty(Text) = "empty";
757		/// The catalog's size.
758		total(u64) = "total";
759		/// The catalog.
760		items(Vec<PickerItem>) = "items";
761		/// Items added to the catalog.
762		items_add(Vec<PickerItem>) = "itemsAdd";
763		/// Item ids removed from the catalog.
764		items_del(Vec<String>) = "itemsDel";
765		/// What to show, in order.
766		order(Vec<OrderEntry>) = "order";
767		/// The selected item.
768		selected(String) = "selected";
769		/// Items marked current.
770		current(Vec<String>) = "current";
771		/// Match ranges per item id.
772		hits(std::collections::BTreeMap<String, Vec<[usize; 2]>>) = "hits";
773		/// Fact columns.
774		columns(Vec<PickerColumn>) = "columns";
775		/// A confirm strip over the selected row.
776		confirm(Confirm) = "confirm";
777		/// A scope column.
778		scopes(Vec<Scope>) = "scopes";
779		/// The active scope.
780		scope(String) = "scope";
781		/// A tab row.
782		tabs(Vec<PickerTab>) = "tabs";
783		/// The active tab.
784		tab(String) = "tab";
785		/// A chip strip.
786		strip(Strip) = "strip";
787		/// The action bar.
788		actions(Vec<PickerAction>) = "actions";
789	}
790}
791
792/// A picker sheet.
793pub fn picker<M>() -> Picker<M> {
794	new("picker")
795}
796
797// ---------------------------------------------------------------------------
798// Progress and motion
799// ---------------------------------------------------------------------------
800
801common_kind!(
802	/// `spinner`: an activity indicator.
803	Spinner
804);
805
806impl<M> Spinner<M> {
807	props! {
808		/// The indicator.
809		style(SpinnerStyle) = "style";
810		/// Text after it.
811		label(Text) = "label";
812	}
813}
814
815/// An activity indicator.
816pub fn spinner<M>() -> Spinner<M> {
817	new("spinner")
818}
819
820common_kind!(
821	/// `shimmer`: a shimmering label.
822	Shimmer
823);
824
825impl<M> Shimmer<M> {
826	props! {
827		/// How it sweeps.
828		mode(ShimmerMode) = "mode";
829		/// The sweep colors.
830		palette(ShimmerPalette) = "palette";
831	}
832
833	/// Replaces the text (plain or spans).
834	pub fn text(mut self, text: impl Into<Text>) -> Self {
835		self.0.set_text(text.into());
836		self
837	}
838}
839
840/// A shimmering label.
841pub fn shimmer<M>(text: impl Into<Text>) -> Shimmer<M> {
842	new::<M, Shimmer<M>>("shimmer").text(text)
843}
844
845common_kind!(
846	/// `elapsed`: a live timer Tern clocks.
847	Elapsed
848);
849
850impl<M> Elapsed<M> {
851	props! {
852		/// Milliseconds already elapsed (negative: a countdown).
853		age(i64) = "age";
854		/// Freezes the display at this many ms.
855		stopped(u64) = "stopped";
856		/// How the time reads.
857		format(ElapsedFormat) = "format";
858	}
859
860	/// Sets `age` from when the timed work started.
861	pub fn since(self, start: Instant) -> Self {
862		let age = i64::try_from(start.elapsed().as_millis()).unwrap_or(i64::MAX);
863		self.age(age)
864	}
865}
866
867/// A live timer.
868pub fn elapsed<M>() -> Elapsed<M> {
869	new("elapsed")
870}
871
872common_kind!(
873	/// `rate`: a number easing between updates.
874	Rate
875);
876
877impl<M> Rate<M> {
878	props! {
879		/// The target value.
880		value(f64) = "value";
881		/// Appended after a space.
882		unit(String) = "unit";
883	}
884}
885
886/// An easing number.
887pub fn rate<M>(value: f64) -> Rate<M> {
888	new::<M, Rate<M>>("rate").value(value)
889}
890
891common_kind!(
892	/// `progress`: a progress bar (indeterminate without a value).
893	Progress
894);
895
896impl<M> Progress<M> {
897	props! {
898		/// Fraction done, 0–1.
899		value(f64) = "value";
900		/// Text after the bar.
901		label(Text) = "label";
902	}
903}
904
905/// A progress bar.
906pub fn progress<M>() -> Progress<M> {
907	new("progress")
908}
909
910common_kind!(
911	/// `meter`: a value as a bar, ring or grid.
912	Meter
913);
914
915impl<M> Meter<M> {
916	props! {
917		/// The level, 0–1.
918		value(f64) = "value";
919		/// Stacked parts.
920		parts(Vec<Part>) = "parts";
921		/// Tint levels.
922		thresholds(Thresholds) = "thresholds";
923		/// The shape.
924		style(MeterStyle) = "style";
925		/// The size.
926		size(Size) = "size";
927		/// `blocks`: one row of this many cells.
928		steps(u32) = "steps";
929		/// Ticks.
930		marks(Vec<MeterMark>) = "marks";
931		/// Text after the shape.
932		label(Text) = "label";
933		/// Text after the label.
934		total(Text) = "total";
935	}
936}
937
938/// A meter.
939pub fn meter<M>() -> Meter<M> {
940	new("meter")
941}
942
943common_kind!(
944	/// `chart`: bars, a sparkline or a heatmap.
945	Chart
946);
947
948impl<M> Chart<M> {
949	props! {
950		/// Which chart.
951		kind(ChartKind) = "kind";
952		/// The size.
953		size(Size) = "size";
954		/// The fill color token.
955		token(String) = "token";
956		/// A line under the chart.
957		summary(Text) = "summary";
958		/// `bars`/`spark`: one bar per entry.
959		series(Vec<Point>) = "series";
960		/// `heatmap`: rows of intensities 0–1.
961		cells(Vec<Vec<Option<f64>>>) = "cells";
962		/// `heatmap`: a tooltip per cell.
963		tips(Vec<Vec<Option<String>>>) = "tips";
964		/// `heatmap`: a label per row.
965		rows(Vec<String>) = "rows";
966		/// `heatmap`: column labels.
967		cols(Vec<ChartCol>) = "cols";
968	}
969}
970
971/// A chart.
972pub fn chart<M>(kind: impl Into<ChartKind>) -> Chart<M> {
973	new::<M, Chart<M>>("chart").kind(kind)
974}
975
976common_kind!(
977	/// `effort`: a thinking-effort glyph.
978	Effort
979);
980
981impl<M> Effort<M> {
982	props! {
983		/// The rung.
984		level(EffortLevel) = "level";
985	}
986}
987
988/// A thinking-effort glyph.
989pub fn effort<M>(level: impl Into<EffortLevel>) -> Effort<M> {
990	new::<M, Effort<M>>("effort").level(level)
991}
992
993// ---------------------------------------------------------------------------
994// Inputs
995// ---------------------------------------------------------------------------
996
997common_kind!(
998	/// `editor`: a multi-line text field whose text the program owns.
999	Editor
1000);
1001
1002impl<M> Editor<M> {
1003	props! {
1004		/// The text.
1005		text(String) = "text";
1006		/// The caret, UTF-16.
1007		cursor(usize) = "cursor";
1008		/// The selection anchor, UTF-16.
1009		anchor(usize) = "anchor";
1010		/// Styled ranges.
1011		decor(Vec<Decor>) = "decor";
1012		/// An inline completion after the caret.
1013		ghost(String) = "ghost";
1014		/// Dim text while empty.
1015		placeholder(String) = "placeholder";
1016		/// Spans before the first line.
1017		prompt(Text) = "prompt";
1018		/// A vim mode label.
1019		mode(String) = "mode";
1020		/// The text is code in this language.
1021		lang(String) = "lang";
1022		/// Not editable.
1023		readonly(bool) = "readonly";
1024		/// Accepts an atomic `send`.
1025		sendable(bool) = "sendable";
1026		/// Cap the height at this many lines.
1027		max_lines(u32) = "maxLines";
1028	}
1029}
1030
1031/// A multi-line text field.
1032pub fn editor<M>() -> Editor<M> {
1033	new("editor")
1034}
1035
1036common_kind!(
1037	/// `input`: a single-line text field.
1038	Input
1039);
1040
1041impl<M> Input<M> {
1042	props! {
1043		/// The text.
1044		text(String) = "text";
1045		/// The caret, UTF-16.
1046		cursor(usize) = "cursor";
1047		/// The selection anchor, UTF-16.
1048		anchor(usize) = "anchor";
1049		/// Styled ranges.
1050		decor(Vec<Decor>) = "decor";
1051		/// An inline completion after the caret.
1052		ghost(String) = "ghost";
1053		/// Dim text while empty.
1054		placeholder(String) = "placeholder";
1055		/// Spans before the text.
1056		prompt(Text) = "prompt";
1057		/// A vim mode label.
1058		mode(String) = "mode";
1059		/// The text is code in this language.
1060		lang(String) = "lang";
1061		/// Not editable.
1062		readonly(bool) = "readonly";
1063		/// Accepts an atomic `send`.
1064		sendable(bool) = "sendable";
1065	}
1066}
1067
1068/// A single-line text field.
1069pub fn input<M>() -> Input<M> {
1070	new("input")
1071}
1072
1073// ---------------------------------------------------------------------------
1074// Bars, toasts and overlays
1075// ---------------------------------------------------------------------------
1076
1077container_kind!(
1078	/// `status`: a status bar strip of `seg`s.
1079	StatusBar
1080);
1081
1082impl<M> StatusBar<M> {
1083	props! {
1084		/// No fill.
1085		transparent(bool) = "transparent";
1086	}
1087}
1088
1089/// A status bar strip.
1090pub fn status<M>() -> StatusBar<M> {
1091	new("status")
1092}
1093
1094container_kind!(
1095	/// `seg`: a status bar segment.
1096	Seg
1097);
1098
1099impl<M> Seg<M> {
1100	props! {
1101		/// An icon.
1102		icon(Icon) = "icon";
1103		/// The right group.
1104		side(Side) = "side";
1105		/// Drop order: lower drops first.
1106		priority(f64) = "priority";
1107	}
1108
1109	/// Replaces the text (plain or spans).
1110	pub fn text(mut self, text: impl Into<Text>) -> Self {
1111		self.0.set_text(text.into());
1112		self
1113	}
1114}
1115
1116/// A status bar segment.
1117pub fn seg<M>(text: impl Into<Text>) -> Seg<M> {
1118	new::<M, Seg<M>>("seg").text(text)
1119}
1120
1121common_kind!(
1122	/// `toast`: a transient notice.
1123	Toast
1124);
1125
1126impl<M> Toast<M> {
1127	props! {
1128		/// The message.
1129		text(String) = "text";
1130		/// A mono detail.
1131		#[allow(clippy::should_implement_trait, reason = "the prop is named `sub`")]
1132		sub(String) = "sub";
1133		/// Take it down after this many ms.
1134		ttl(u64) = "ttl";
1135	}
1136}
1137
1138/// A transient notice.
1139pub fn toast<M>(text: impl Into<String>) -> Toast<M> {
1140	new::<M, Toast<M>>("toast").text(text)
1141}
1142
1143container_kind!(
1144	/// `overlay`: a floating panel or sheet in `layer`.
1145	Overlay
1146);
1147
1148impl<M> Overlay<M> {
1149	props! {
1150		/// Placement.
1151		anchor(Anchor) = "anchor";
1152		/// Card width or full sheet.
1153		size(OverlaySize) = "size";
1154		/// A dim backdrop taking the pointer.
1155		modal(bool) = "modal";
1156		/// The title row.
1157		head(Text) = "head";
1158	}
1159}
1160
1161/// A floating panel.
1162pub fn overlay<M>() -> Overlay<M> {
1163	new("overlay")
1164}
1165
1166// ---------------------------------------------------------------------------
1167// Tool, agent and task kinds
1168// ---------------------------------------------------------------------------
1169
1170kind_type!(
1171	/// `tool`: one coding-agent tool call.
1172	Tool
1173);
1174
1175impl<M> Tool<M> {
1176	base_methods!();
1177
1178	max_method!();
1179
1180	actions_methods!();
1181
1182	event_methods!();
1183
1184	children_methods!();
1185
1186	props! {
1187		/// The tool's name (picks the icon).
1188		name(String) = "name";
1189		/// The verb.
1190		title(Text) = "title";
1191		/// The primary argument.
1192		target(Text) = "target";
1193		/// How a string target is drawn.
1194		target_kind(TargetKind) = "targetKind";
1195		/// Short facts after the target.
1196		meta(Vec<Text>) = "meta";
1197		/// Chips in the head.
1198		badges(Vec<BadgeSpec>) = "badges";
1199		/// A short state note.
1200		note(Text) = "note";
1201		/// A non-zero exit code.
1202		exit(i64) = "exit";
1203		/// The state.
1204		status(Status) = "status";
1205		/// Elapsed ms when sent.
1206		age(i64) = "age";
1207		/// The final duration in ms.
1208		took(u64) = "took";
1209		/// The head's tooltip.
1210		intent(String) = "intent";
1211		/// Ring or no ring.
1212		frame(ToolFrame) = "frame";
1213		/// The head toggles the body.
1214		collapsible(bool) = "collapsible";
1215		/// The initial fold.
1216		collapsed(bool) = "collapsed";
1217		/// The clamp of the collapsed body.
1218		preview(ToolPreview) = "preview";
1219		/// Hover buttons.
1220		tools(Vec<ToolButton>) = "tools";
1221	}
1222}
1223
1224/// A tool call.
1225pub fn tool<M>(name: impl Into<String>) -> Tool<M> {
1226	new::<M, Tool<M>>("tool").name(name)
1227}
1228
1229container_kind!(
1230	/// `agent`: one subagent row.
1231	Agent
1232);
1233
1234impl<M> Agent<M> {
1235	props! {
1236		/// The display name.
1237		name(String) = "name";
1238		/// The agent type.
1239		agent(String) = "agent";
1240		/// More chips.
1241		badges(Vec<BadgeSpec>) = "badges";
1242		/// The one-line task.
1243		task(Text) = "task";
1244		/// The state.
1245		status(AgentStatus) = "status";
1246		/// The model.
1247		model(String) = "model";
1248		/// The model dot's palette token.
1249		thinking(String) = "thinking";
1250		/// Stats.
1251		stats(AgentStats) = "stats";
1252		/// The tool running now.
1253		tool(AgentTool) = "tool";
1254		/// A retry countdown.
1255		retry(Retry) = "retry";
1256		/// Nesting depth.
1257		depth(u32) = "depth";
1258		/// The row toggles the children.
1259		collapsible(bool) = "collapsible";
1260		/// The initial fold.
1261		collapsed(bool) = "collapsed";
1262	}
1263}
1264
1265/// A subagent row.
1266pub fn agent<M>(name: impl Into<String>) -> Agent<M> {
1267	new::<M, Agent<M>>("agent").name(name)
1268}
1269
1270common_kind!(
1271	/// `checklist`: a todo list.
1272	Checklist
1273);
1274
1275impl<M> Checklist<M> {
1276	props! {
1277		/// The phases.
1278		phases(Vec<Phase>) = "phases";
1279		/// The presentation.
1280		mode(ChecklistMode) = "mode";
1281		/// `reminder`: text after the count.
1282		note(Text) = "note";
1283	}
1284}
1285
1286/// A todo list.
1287pub fn checklist<M>() -> Checklist<M> {
1288	new("checklist")
1289}
1290
1291kind_type!(
1292	/// `prefs`: a settings page or sheet.
1293	Prefs
1294);
1295
1296impl<M> Prefs<M> {
1297	base_methods!();
1298
1299	max_method!();
1300
1301	actions_methods!();
1302
1303	event_methods!();
1304
1305	children_methods!();
1306
1307	props! {
1308		/// The brand title.
1309		title(String) = "title";
1310		/// Nav rows.
1311		pages(Vec<PrefsPage>) = "pages";
1312		/// The current page.
1313		page(String) = "page";
1314		/// A paragraph under the page title.
1315		lead(String) = "lead";
1316		/// The sections.
1317		sections(Vec<PrefsSection>) = "sections";
1318		/// Search text.
1319		query(String) = "query";
1320		/// The caret in the search field, UTF-16.
1321		cursor(usize) = "cursor";
1322		/// The row or section with the focus ring.
1323		focus(String) = "focus";
1324		/// The row being edited.
1325		editing(Editing) = "editing";
1326	}
1327}
1328
1329/// A settings page.
1330pub fn prefs<M>() -> Prefs<M> {
1331	new("prefs")
1332}
1333
1334/// Appends `value` to the list prop `key`.
1335fn push<M>(node: &mut Node<M>, key: &str, value: &impl serde::Serialize) {
1336	let Ok(value) = serde_json::to_value(value) else {
1337		return;
1338	};
1339	match node.props.get_mut(key) {
1340		Some(serde_json::Value::Array(list)) => list.push(value),
1341		_ => {
1342			node
1343				.props
1344				.insert(key.to_owned(), serde_json::Value::Array(vec![value]));
1345		},
1346	}
1347}
1348
1349#[cfg(test)]
1350mod tests {
1351	use serde_json::json;
1352
1353	use super::*;
1354
1355	fn wire<M>(node: impl Into<Node<M>>) -> serde_json::Value {
1356		let node = node.into();
1357		json!({ "k": node.kind, "p": node.props })
1358	}
1359
1360	#[test]
1361	fn text_takes_a_string_or_spans() {
1362		assert_eq!(wire::<()>(text("hi")), json!({"k": "text", "p": {"text": "hi"}}));
1363		let styled = text::<()>(spans!["a", span("b").style(Token::Muted).style("strong")]);
1364		assert_eq!(
1365			wire(styled.clone()),
1366			json!({"k": "text", "p": {"spans": [{"t": "a"}, {"t": "b", "s": "muted strong"}]}})
1367		);
1368		// Switching back to plain text drops the spans.
1369		assert_eq!(wire(styled.text("c")), json!({"k": "text", "p": {"text": "c"}}));
1370	}
1371
1372	#[test]
1373	fn typed_values_and_escape_hatches_serialize() {
1374		let c = card::<()>()
1375			.head("Deploy")
1376			.status(Status::Running)
1377			.tone("brand-new")
1378			.min(Bound::w(Extent::Ch(10.0)))
1379			.basis(Basis::Content)
1380			.key(7)
1381			.prop("future", json!([1]))
1382			.prop("head", serde_json::Value::Null);
1383		assert_eq!(
1384			wire(c),
1385			json!({"k": "card", "p": {
1386				"status": "running", "tone": "brand-new", "min": {"w": "10ch"},
1387				"basis": "content", "key": 7, "future": [1]
1388			}})
1389		);
1390	}
1391
1392	#[test]
1393	fn kind_specific_props_replace_common_meanings() {
1394		let title = spans![span("Heading").style("strong")];
1395		assert_eq!(
1396			wire(tool::<()>("read").title(title.clone()))["p"]["title"],
1397			json!([{"t": "Heading", "s": "strong"}])
1398		);
1399		assert_eq!(
1400			wire(picker::<()>().title(title).actions(vec![PickerAction {
1401				id: "open".into(),
1402				primary: Some(true),
1403				..PickerAction::default()
1404			}]))["p"],
1405			json!({
1406				"title": [{"t": "Heading", "s": "strong"}],
1407				"actions": [{"id": "open", "primary": true}]
1408			})
1409		);
1410		assert_eq!(wire(prefs::<()>().title("Settings"))["p"]["title"], "Settings");
1411		assert_eq!(
1412			wire(list::<()>().max_fraction(0.5).max_lines(4))["p"]["max"],
1413			json!({"lines": 4})
1414		);
1415		assert_eq!(wire(list::<()>().max_lines(4).max_fraction(0.5))["p"]["max"], 0.5);
1416	}
1417
1418	#[test]
1419	fn agent_statuses_have_their_own_vocabulary() {
1420		for (status, expected) in [
1421			(AgentStatus::Pending, "pending"),
1422			(AgentStatus::Running, "running"),
1423			(AgentStatus::Done, "done"),
1424			(AgentStatus::Failed, "failed"),
1425			(AgentStatus::Aborted, "aborted"),
1426			(AgentStatus::Idle, "idle"),
1427			(AgentStatus::Parked, "parked"),
1428			(AgentStatus::Other("future".into()), "future"),
1429		] {
1430			assert_eq!(wire(agent::<()>("worker").status(status))["p"]["status"], expected);
1431		}
1432	}
1433
1434	#[test]
1435	fn table_meter_cells_keep_the_wire_wrapper() {
1436		let node = wire(table::<()>().row(TableRow::new("a").cell("usage", MeterCell {
1437			value: Some(0.5),
1438			title: Some("Half".into()),
1439			tone: Some(Tone::Success),
1440			..MeterCell::default()
1441		})));
1442		assert_eq!(
1443			node["p"]["rows"][0]["cells"]["usage"],
1444			json!({
1445				"meter": {"value": 0.5, "title": "Half", "tone": "success"}
1446			})
1447		);
1448		assert_eq!(
1449			serde_json::to_value(Cell::Meter(MeterCell::default())).unwrap(),
1450			json!({"meter": {}})
1451		);
1452	}
1453
1454	#[test]
1455	fn list_props_append() {
1456		let k = kv::<()>().item("a", "1").item("b", span("2").style("num"));
1457		assert_eq!(
1458			wire(k),
1459			json!({"k": "kv", "p": {"items": [
1460				{"k": "a", "v": "1"}, {"k": "b", "v": [{"t": "2", "s": "num"}]}
1461			]}})
1462		);
1463	}
1464}