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,
42	min_method, props, 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			min_method!();
98			title_method!();
99			max_method!();
100			actions_methods!();
101			event_methods!();
102		}
103	};
104}
105
106/// Defines a container kind's builder with the common props.
107macro_rules! container_kind {
108	($(#[$m:meta])* $name:ident) => {
109		common_kind!($(#[$m])* $name);
110
111		impl<M> $name<M> {
112			children_methods!();
113		}
114	};
115}
116
117/// A new builder of `kind`.
118fn new<M, T: From<Node<M>>>(kind: &str) -> T {
119	T::from(Node::new(kind))
120}
121
122/// Implements `From<Node>` for builder types (used by [`new`]).
123macro_rules! from_node {
124	($($name:ident),*) => {
125		$(impl<M> From<Node<M>> for $name<M> {
126			fn from(node: Node<M>) -> Self {
127				Self(node)
128			}
129		})*
130	};
131}
132
133from_node!(
134	Col,
135	Row,
136	Card,
137	Section,
138	Rule,
139	Spacer,
140	TextNode,
141	Md,
142	Code,
143	Diff,
144	Ansi,
145	Rows,
146	Math,
147	Kv,
148	Table,
149	Tree,
150	Json,
151	Badge,
152	Kbd,
153	IconNode,
154	Image,
155	Media,
156	List,
157	Item,
158	Tabs,
159	Picker,
160	Spinner,
161	Shimmer,
162	Elapsed,
163	Rate,
164	Progress,
165	Meter,
166	Chart,
167	Effort,
168	Editor,
169	Input,
170	StatusBar,
171	Seg,
172	Toast,
173	Overlay,
174	Tool,
175	Agent,
176	Checklist,
177	Prefs,
178	Button,
179	Stats,
180	Select,
181	Switch,
182	Slider,
183	Field,
184	Otp,
185	Buttons,
186	Avatar,
187	Avatars,
188	Breadcrumb,
189	Pager,
190	Empty,
191	Skeleton,
192	Calendar,
193	Date,
194	Menu,
195	Menubar,
196	Nav,
197	Split,
198	Scroll,
199	Carousel,
200	Message,
201	Attachment,
202	Trace,
203	Stacktrace,
204	Suggestions,
205	Questions
206);
207
208/// What a slot prop (`card` `head`/`end`, `section` `head`, `overlay`
209/// `end`/`foot`) holds: text, or a node drawn there.
210pub enum Slot<M = ()> {
211	/// Text or spans (only `head` takes text).
212	Text(Text),
213	/// A node: added as the child keyed by the slot's name, the prop sent as
214	/// its id (`<parent>.end`).
215	Node(Node<M>),
216}
217
218impl<M> From<&str> for Slot<M> {
219	fn from(t: &str) -> Self {
220		Self::Text(t.into())
221	}
222}
223
224impl<M> From<String> for Slot<M> {
225	fn from(t: String) -> Self {
226		Self::Text(t.into())
227	}
228}
229
230impl<M> From<&String> for Slot<M> {
231	fn from(t: &String) -> Self {
232		Self::Text(t.into())
233	}
234}
235
236impl<M, const N: usize> From<[Span; N]> for Slot<M> {
237	fn from(s: [Span; N]) -> Self {
238		Self::Text(s.into())
239	}
240}
241
242impl<M> From<Text> for Slot<M> {
243	fn from(t: Text) -> Self {
244		Self::Text(t)
245	}
246}
247
248impl<M> From<Span> for Slot<M> {
249	fn from(s: Span) -> Self {
250		Self::Text(s.into())
251	}
252}
253
254impl<M> From<Vec<Span>> for Slot<M> {
255	fn from(s: Vec<Span>) -> Self {
256		Self::Text(s.into())
257	}
258}
259
260impl<M> From<Node<M>> for Slot<M> {
261	fn from(n: Node<M>) -> Self {
262		Self::Node(n)
263	}
264}
265
266/// Builders of nodes fill slots too.
267macro_rules! slot_from {
268	($($name:ident),*) => {
269		$(impl<M> From<$name<M>> for Slot<M> {
270			fn from(n: $name<M>) -> Self {
271				Self::Node(n.0)
272			}
273		})*
274	};
275}
276
277slot_from!(
278	Col,
279	Row,
280	Card,
281	Section,
282	Rule,
283	Spacer,
284	TextNode,
285	Md,
286	Code,
287	Diff,
288	Ansi,
289	Rows,
290	Math,
291	Kv,
292	Table,
293	Tree,
294	Json,
295	Badge,
296	Kbd,
297	IconNode,
298	Image,
299	Media,
300	List,
301	Item,
302	Tabs,
303	Picker,
304	Spinner,
305	Shimmer,
306	Elapsed,
307	Rate,
308	Progress,
309	Meter,
310	Chart,
311	Effort,
312	Editor,
313	Input,
314	StatusBar,
315	Seg,
316	Toast,
317	Overlay,
318	Tool,
319	Agent,
320	Checklist,
321	Prefs,
322	Button,
323	Stats,
324	Select,
325	Switch,
326	Slider,
327	Field,
328	Otp,
329	Buttons,
330	Avatar,
331	Avatars,
332	Breadcrumb,
333	Pager,
334	Empty,
335	Skeleton,
336	Calendar,
337	Date,
338	Menu,
339	Menubar,
340	Nav,
341	Split,
342	Scroll,
343	Carousel,
344	Message,
345	Attachment,
346	Trace,
347	Stacktrace,
348	Suggestions,
349	Questions
350);
351
352/// Sets slot prop `slot` of `node` from text or a node.
353fn set_slot<M>(node: &mut Node<M>, slot: &'static str, value: Slot<M>) {
354	match value {
355		Slot::Text(text) => {
356			node.clear_slot(slot);
357			node.set(slot, &text);
358		},
359		Slot::Node(child) => node.set_slot(slot, child),
360	}
361}
362
363/// Card/section/overlay head props shared by the panel kinds.
364macro_rules! panel_head_methods {
365	() => {
366		/// The head icon (wins over the role's).
367		pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
368			self.0.set("icon", &icon.into());
369			self
370		}
371
372		/// Muted facts after the head text.
373		pub fn meta(mut self, meta: impl Into<Text>) -> Self {
374			self.0.set("meta", &meta.into());
375			self
376		}
377
378		/// A node drawn at the head's end (buttons, segmented tabs, badges);
379		/// clicks on it never fold the panel.
380		pub fn end(mut self, end: impl Into<Slot<M>>) -> Self {
381			set_slot(&mut self.0, "end", end.into());
382			self
383		}
384	};
385}
386
387// ---------------------------------------------------------------------------
388// Layout
389// ---------------------------------------------------------------------------
390
391container_kind!(
392	/// `col`: a vertical stack.
393	Col
394);
395
396impl<M> Col<M> {
397	props! {
398		/// Space between children.
399		gap(Gap) = "gap";
400		/// Cross-axis alignment.
401		align(Align) = "align";
402		/// Main-axis distribution.
403		justify(Justify) = "justify";
404		/// Adds class `wrap`.
405		wrap(bool) = "wrap";
406	}
407}
408
409/// A vertical stack.
410pub fn col<M>() -> Col<M> {
411	new("col")
412}
413
414container_kind!(
415	/// `row`: a horizontal flex row.
416	Row
417);
418
419impl<M> Row<M> {
420	props! {
421		/// Space between children.
422		gap(Gap) = "gap";
423		/// Cross-axis alignment.
424		align(Align) = "align";
425		/// Main-axis distribution.
426		justify(Justify) = "justify";
427		/// Children flow onto more lines when they don't fit.
428		wrap(bool) = "wrap";
429	}
430}
431
432/// A horizontal flex row.
433pub fn row<M>() -> Row<M> {
434	new("row")
435}
436
437container_kind!(
438	/// `card`: a rounded card with a head and a body, optionally folding.
439	Card
440);
441
442impl<M> Card<M> {
443	panel_head_methods!();
444
445	props! {
446		/// The status chip.
447		status(Status) = "status";
448		/// The head flips the fold.
449		collapsible(bool) = "collapsible";
450		/// The starting fold state.
451		collapsed(bool) = "collapsed";
452		/// Lines shown while collapsed.
453		preview(Preview) = "preview";
454		/// An accent ring and halo.
455		selected(bool) = "selected";
456		/// The body has no padding.
457		inset(bool) = "inset";
458		/// The bare variant.
459		variant(CardVariant) = "variant";
460	}
461
462	/// Head content: text, spans, or a node (sent as the child `head`).
463	pub fn head(mut self, head: impl Into<Slot<M>>) -> Self {
464		set_slot(&mut self.0, "head", head.into());
465		self
466	}
467}
468
469/// A card.
470pub fn card<M>() -> Card<M> {
471	new("card")
472}
473
474container_kind!(
475	/// `section`: a lighter disclosure group.
476	Section
477);
478
479impl<M> Section<M> {
480	props! {
481		/// The head flips the fold.
482		collapsible(bool) = "collapsible";
483		/// The starting fold state.
484		collapsed(bool) = "collapsed";
485	}
486
487	/// Head content: text, spans, or a node (sent as the child `head`).
488	pub fn head(mut self, head: impl Into<Slot<M>>) -> Self {
489		set_slot(&mut self.0, "head", head.into());
490		self
491	}
492}
493
494/// A section.
495pub fn section<M>() -> Section<M> {
496	new("section")
497}
498
499common_kind!(
500	/// `rule`: a divider hairline.
501	Rule
502);
503
504impl<M> Rule<M> {
505	props! {
506		/// The label: centered (`line`), in a framed row (`note`) or a pill
507		/// (`pin`).
508		label(Text) = "label";
509		/// The look.
510		variant(RuleVariant) = "variant";
511		/// An icon before the label.
512		icon(Icon) = "icon";
513		/// A time or short stamp.
514		time(String) = "time";
515	}
516
517	/// A node drawn at the end: after the label (`line`), at the row's
518	/// right (`note`) or beside the pill (`pin`).
519	pub fn end(mut self, end: impl Into<Slot<M>>) -> Self {
520		set_slot(&mut self.0, "end", end.into());
521		self
522	}
523}
524
525/// A divider.
526pub fn rule<M>() -> Rule<M> {
527	new("rule")
528}
529
530common_kind!(
531	/// `spacer`: vertical space.
532	Spacer
533);
534
535impl<M> Spacer<M> {
536	props! {
537		/// The height.
538		size(Gap) = "size";
539	}
540}
541
542/// Vertical space.
543pub fn spacer<M>() -> Spacer<M> {
544	new("spacer")
545}
546
547// ---------------------------------------------------------------------------
548// Text and code
549// ---------------------------------------------------------------------------
550
551common_kind!(
552	/// `text`: styled spans, wrapped or truncated.
553	TextNode
554);
555
556impl<M> TextNode<M> {
557	props! {
558		/// How it wraps.
559		wrap(Wrap) = "wrap";
560		/// Where it is cut.
561		truncate(Truncate) = "truncate";
562		/// Clamp to this many lines.
563		lines(u32) = "lines";
564		/// Caps the line length.
565		measure(Measure) = "measure";
566		/// How cited spans look.
567		cite(CiteStyle) = "cite";
568		/// Draws the numbered sources list under the text (default only
569		/// for `note` cites).
570		sources(bool) = "sources";
571	}
572
573	/// Replaces the text (plain or spans).
574	pub fn text(mut self, text: impl Into<Text>) -> Self {
575		self.0.set_text(text.into());
576		self
577	}
578}
579
580/// Text, plain or styled.
581pub fn text<M>(text: impl Into<Text>) -> TextNode<M> {
582	let mut node = Node::new("text");
583	node.set_text(text.into());
584	TextNode(node)
585}
586
587common_kind!(
588	/// `md`: Markdown.
589	Md
590);
591
592impl<M> Md<M> {
593	props! {
594		/// The Markdown source.
595		text(String) = "text";
596		/// The source is still arriving.
597		stream(bool) = "stream";
598		/// Spans drawn in place of their literal text.
599		marks(Vec<Span>) = "marks";
600	}
601}
602
603/// Markdown.
604pub fn md<M>(source: impl Into<String>) -> Md<M> {
605	new::<M, Md<M>>("md").text(source)
606}
607
608common_kind!(
609	/// `code`: highlighted code.
610	Code
611);
612
613impl<M> Code<M> {
614	props! {
615		/// The code.
616		text(String) = "text";
617		/// The grammar by language name or extension.
618		lang(String) = "lang";
619		/// A file path: picks the grammar and shows a header.
620		path(String) = "path";
621		/// Show line numbers.
622		numbers(bool) = "numbers";
623		/// The first line's number.
624		start(i64) = "start";
625		/// Each line's number, strictly ascending, in place of counting from
626		/// `start`: a `…` row goes where the numbers jump (search hits,
627		/// excerpts).
628		nums(Vec<u64>) = "nums";
629		/// Marked lines.
630		marks(Vec<CodeMark>) = "marks";
631		/// Soft-wrap long lines.
632		wrap(bool) = "wrap";
633		/// Runs of lines drawn folded: sorted, not overlapping.
634		folds(Vec<CodeFold>) = "folds";
635		/// A strip above the lines of their heat and tones, with a bracket
636		/// over the lines in view; a click or drag on it scrolls there.
637		overview(bool) = "overview";
638	}
639}
640
641/// Highlighted code.
642pub fn code<M>(source: impl Into<String>) -> Code<M> {
643	new::<M, Code<M>>("code").text(source)
644}
645
646common_kind!(
647	/// `diff`: a unified or split diff.
648	Diff
649);
650
651impl<M> Diff<M> {
652	props! {
653		/// A unified diff.
654		text(String) = "text";
655		/// Hunks given directly.
656		hunks(Vec<Hunk>) = "hunks";
657		/// Picks the grammar.
658		path(String) = "path";
659		/// The grammar by language name.
660		lang(String) = "lang";
661		/// How it lays out.
662		mode(DiffMode) = "mode";
663	}
664}
665
666/// A diff.
667pub fn diff<M>() -> Diff<M> {
668	new("diff")
669}
670
671common_kind!(
672	/// `ansi`: output with escape sequences.
673	Ansi
674);
675
676impl<M> Ansi<M> {
677	props! {
678		/// The output.
679		text(String) = "text";
680		/// Wrap at this many columns.
681		cols(u32) = "cols";
682		/// Keep the tail in view.
683		follow(bool) = "follow";
684	}
685
686	/// Clamps to `lines` lines under a fade with a "more lines" button.
687	pub fn preview(mut self, lines: u32) -> Self {
688		self
689			.0
690			.set("preview", &serde_json::json!({ "lines": lines }));
691		self
692	}
693}
694
695/// Terminal output.
696pub fn ansi<M>(output: impl Into<String>) -> Ansi<M> {
697	new::<M, Ansi<M>>("ansi").text(output)
698}
699
700common_kind!(
701	/// `rows`: pre-rendered ANSI rows (the migration fallback).
702	Rows
703);
704
705impl<M> Rows<M> {
706	props! {
707		/// The rows.
708		lines(Vec<String>) = "lines";
709		/// The exact width in columns.
710		cols(u32) = "cols";
711	}
712}
713
714/// Pre-rendered rows.
715pub fn rows<M>(lines: impl Into<Vec<String>>) -> Rows<M> {
716	new::<M, Rows<M>>("rows").lines(lines)
717}
718
719common_kind!(
720	/// `math`: TeX math.
721	Math
722);
723
724impl<M> Math<M> {
725	props! {
726		/// The TeX, without delimiters.
727		text(String) = "text";
728		/// Display style.
729		display(bool) = "display";
730	}
731}
732
733/// TeX math.
734pub fn math<M>(tex: impl Into<String>) -> Math<M> {
735	new::<M, Math<M>>("math").text(tex)
736}
737
738// ---------------------------------------------------------------------------
739// Data
740// ---------------------------------------------------------------------------
741
742common_kind!(
743	/// `kv`: an aligned key/value list.
744	Kv
745);
746
747impl<M> Kv<M> {
748	props! {
749		/// All pairs.
750		items(Vec<KvItem>) = "items";
751		/// How it lays out.
752		layout(KvLayout) = "layout";
753	}
754
755	/// Adds a pair.
756	pub fn item(mut self, k: impl Into<Text>, v: impl Into<Text>) -> Self {
757		push(&mut self.0, "items", &KvItem { k: k.into(), v: v.into() });
758		self
759	}
760}
761
762/// A key/value list.
763pub fn kv<M>() -> Kv<M> {
764	new("kv")
765}
766
767common_kind!(
768	/// `table`: a table with priority-hidden columns and meter cells.
769	Table
770);
771
772impl<M> Table<M> {
773	props! {
774		/// All columns.
775		cols(Vec<Column>) = "cols";
776		/// All rows.
777		rows(Vec<TableRow>) = "rows";
778		/// Rows take clicks: `select` on click, `activate` on double click,
779		/// with the row id as `item`.
780		selectable(bool) = "selectable";
781		/// The row drawn selected, kept in view.
782		selected(String) = "selected";
783		/// Shown muted when there are no rows.
784		empty(Text) = "empty";
785		/// The column the rows are sorted by (its header draws the arrow).
786		/// A header click sends `action` `sort` with `{col, dir}`.
787		sort(Sort) = "sort";
788		/// Rows are picked several at a time; each change sends `change`
789		/// with the picked ids.
790		multi(bool) = "multi";
791		/// The ids of the picked rows.
792		picked(Vec<String>) = "picked";
793		/// How picks and the sort look.
794		variant(TableVariant) = "variant";
795		/// Buttons in the selection bar, sent with the picked ids.
796		bar(Vec<ActButton>) = "bar";
797		/// Muted text after the count in the selection bar.
798		hint(Text) = "hint";
799	}
800
801	/// Adds a column.
802	pub fn col(mut self, col: Column) -> Self {
803		push(&mut self.0, "cols", &col);
804		self
805	}
806
807	/// Adds a row.
808	pub fn row(mut self, row: TableRow) -> Self {
809		push(&mut self.0, "rows", &row);
810		self
811	}
812
813	/// Adds a selection bar button.
814	pub fn bar_button(mut self, button: ActButton) -> Self {
815		push(&mut self.0, "bar", &button);
816		self
817	}
818}
819
820/// A table.
821pub fn table<M>() -> Table<M> {
822	new("table")
823}
824
825common_kind!(
826	/// `tree`: a disclosure tree.
827	Tree
828);
829
830impl<M> Tree<M> {
831	props! {
832		/// The top-level items.
833		nodes(Vec<TreeItem>) = "nodes";
834	}
835
836	/// Adds a top-level item.
837	pub fn item(mut self, item: TreeItem) -> Self {
838		push(&mut self.0, "nodes", &item);
839		self
840	}
841}
842
843/// A disclosure tree.
844pub fn tree<M>() -> Tree<M> {
845	new("tree")
846}
847
848common_kind!(
849	/// `json`: a JSON value Tern draws as a foldable tree.
850	Json
851);
852
853impl<M> Json<M> {
854	/// How many levels of containers start expanded (default 0).
855	pub fn open(mut self, levels: u32) -> Self {
856		self.0.set("open", &levels);
857		self
858	}
859}
860
861/// A JSON value Tern draws natively as an interactive tree. Check the hello
862/// reply's `kinds` for `json` and fall back to [`tree`] otherwise.
863pub fn json<M>(value: impl Into<serde_json::Value>) -> Json<M> {
864	let mut node: Json<M> = new("json");
865	node.0.set("value", &value.into());
866	node
867}
868
869common_kind!(
870	/// `badge`: a pill chip.
871	Badge
872);
873
874impl<M> Badge<M> {
875	props! {
876		/// The label (plain text).
877		text(String) = "text";
878	}
879}
880
881/// A pill chip.
882pub fn badge<M>(text: impl Into<String>) -> Badge<M> {
883	new::<M, Badge<M>>("badge").text(text)
884}
885
886common_kind!(
887	/// `kbd`: keycaps.
888	Kbd
889);
890
891impl<M> Kbd<M> {
892	props! {
893		/// Key names, in order.
894		keys(Vec<String>) = "keys";
895	}
896}
897
898/// Keycaps (`["cmd", "K"]`).
899pub fn kbd<M, K: Into<String>>(keys: impl IntoIterator<Item = K>) -> Kbd<M> {
900	new::<M, Kbd<M>>("kbd").keys(keys.into_iter().map(Into::into).collect::<Vec<_>>())
901}
902
903common_kind!(
904	/// `icon`: a named icon.
905	IconNode
906);
907
908impl<M> IconNode<M> {
909	props! {
910		/// The icon.
911		name(Icon) = "name";
912	}
913}
914
915/// A named icon.
916pub fn icon<M>(name: impl Into<Icon>) -> IconNode<M> {
917	new::<M, IconNode<M>>("icon").name(name)
918}
919
920common_kind!(
921	/// `image`: an image from a blob.
922	Image
923);
924
925impl<M> Image<M> {
926	props! {
927		/// A blob id.
928		blob(String) = "blob";
929		/// An image Tern ships (`"omp"`).
930		builtin(String) = "builtin";
931		/// The accessible name.
932		alt(String) = "alt";
933		/// Natural width in px.
934		w(f64) = "w";
935		/// Natural height in px.
936		h(f64) = "h";
937		/// A file the zoom viewer may open.
938		path(String) = "path";
939	}
940}
941
942/// An image.
943pub fn image<M>() -> Image<M> {
944	new("image")
945}
946
947common_kind!(
948	/// `media`: a file Tern loads by path and shows in its media viewer.
949	Media
950);
951
952impl<M> Media<M> {
953	props! {
954		/// A git revision to read the file from (`HEAD`, a sha, `:0` for the index).
955		rev(String) = "rev";
956		/// `media` nodes sharing this link pan and zoom together.
957		link(String) = "link";
958	}
959}
960
961/// A file at `path` (absolute, or relative to the pane's working directory) in
962/// Tern's media viewer.
963pub fn media<M>(path: impl Into<String>) -> Media<M> {
964	let mut node = new::<M, Media<M>>("media");
965	node.0.set("path", &path.into());
966	node
967}
968
969// ---------------------------------------------------------------------------
970// Lists, tabs and pickers
971// ---------------------------------------------------------------------------
972
973kind_type!(
974	/// `list`: a selectable list of `item`s.
975	List
976);
977
978impl<M> List<M> {
979	base_methods!();
980
981	min_method!();
982
983	title_method!();
984
985	actions_methods!();
986
987	event_methods!();
988
989	children_methods!();
990
991	props! {
992		/// The selected item's id.
993		selected(String) = "selected";
994		/// A query marked in each item's label.
995		filter(String) = "filter";
996		/// The line shown when empty.
997		empty(Text) = "empty";
998		/// Virtualize the rows.
999		virtualize(bool) = "virtual";
1000	}
1001
1002	/// Caps the height at `lines` rows (the list scrolls itself).
1003	pub fn max_lines(mut self, lines: u32) -> Self {
1004		self.0.set("max", &serde_json::json!({ "lines": lines }));
1005		self
1006	}
1007
1008	/// Caps the height at a fraction of the window's.
1009	pub fn max_fraction(mut self, fraction: f64) -> Self {
1010		self.0.set("max", &fraction);
1011		self
1012	}
1013}
1014
1015/// A selectable list.
1016pub fn list<M>() -> List<M> {
1017	new("list")
1018}
1019
1020common_kind!(
1021	/// `item`: one list row.
1022	Item
1023);
1024
1025impl<M> Item<M> {
1026	props! {
1027		/// The row's text.
1028		label(Text) = "label";
1029		/// Dim text after the label.
1030		detail(Text) = "detail";
1031		/// Right-aligned text.
1032		value(Text) = "value";
1033		/// An icon.
1034		icon(Icon) = "icon";
1035		/// Key names drawn as keycaps.
1036		hint(Vec<String>) = "hint";
1037		/// Dims the row; it sends nothing.
1038		disabled(bool) = "disabled";
1039	}
1040}
1041
1042/// A list row.
1043pub fn item<M>(label: impl Into<Text>) -> Item<M> {
1044	new::<M, Item<M>>("item").label(label)
1045}
1046
1047common_kind!(
1048	/// `tabs`: a tab strip.
1049	Tabs
1050);
1051
1052impl<M> Tabs<M> {
1053	props! {
1054		/// All tabs.
1055		items(Vec<Tab>) = "items";
1056		/// The active tab's id.
1057		active(String) = "active";
1058		/// Underline (default) or a segmented well.
1059		variant(TabsVariant) = "variant";
1060	}
1061
1062	/// Adds a tab.
1063	pub fn tab(mut self, id: impl Into<String>, label: impl Into<Text>) -> Self {
1064		push(&mut self.0, "items", &Tab { id: id.into(), label: label.into() });
1065		self
1066	}
1067}
1068
1069/// A tab strip.
1070pub fn tabs<M>() -> Tabs<M> {
1071	new("tabs")
1072}
1073
1074kind_type!(
1075	/// `picker`: a searchable picker sheet (its child is the preview).
1076	Picker
1077);
1078
1079impl<M> Picker<M> {
1080	base_methods!();
1081
1082	min_method!();
1083
1084	max_method!();
1085
1086	event_methods!();
1087
1088	children_methods!();
1089
1090	props! {
1091		/// The sheet's size.
1092		size(PickerSize) = "size";
1093		/// The row template.
1094		layout(PickerLayout) = "layout";
1095		/// Where the preview sits.
1096		preview(PickerPreview) = "preview";
1097		/// The heading.
1098		title(Text) = "title";
1099		/// The heading's second part.
1100		subtitle(Text) = "subtitle";
1101		/// The head icon.
1102		icon(Icon) = "icon";
1103		/// The search text (unset hides the search).
1104		query(String) = "query";
1105		/// The caret in `query`, UTF-16.
1106		cursor(usize) = "cursor";
1107		/// The search placeholder.
1108		placeholder(String) = "placeholder";
1109		/// What the rows are.
1110		noun(String) = "noun";
1111		/// Which part the keys drive.
1112		focus(PickerFocus) = "focus";
1113		/// Ready, loading or error.
1114		state(PickerState) = "state";
1115		/// The error card's text.
1116		message(Text) = "message";
1117		/// The text when there are no rows.
1118		empty(Text) = "empty";
1119		/// The catalog's size.
1120		total(u64) = "total";
1121		/// The catalog.
1122		items(Vec<PickerItem>) = "items";
1123		/// Items added to the catalog.
1124		items_add(Vec<PickerItem>) = "itemsAdd";
1125		/// Item ids removed from the catalog.
1126		items_del(Vec<String>) = "itemsDel";
1127		/// What to show, in order.
1128		order(Vec<OrderEntry>) = "order";
1129		/// The selected item.
1130		selected(String) = "selected";
1131		/// Items marked current.
1132		current(Vec<String>) = "current";
1133		/// Match ranges per item id.
1134		hits(std::collections::BTreeMap<String, Vec<[usize; 2]>>) = "hits";
1135		/// Fact columns.
1136		columns(Vec<PickerColumn>) = "columns";
1137		/// A confirm strip over the selected row.
1138		confirm(Confirm) = "confirm";
1139		/// A scope column.
1140		scopes(Vec<Scope>) = "scopes";
1141		/// The active scope.
1142		scope(String) = "scope";
1143		/// A tab row.
1144		tabs(Vec<PickerTab>) = "tabs";
1145		/// The active tab.
1146		tab(String) = "tab";
1147		/// A chip strip.
1148		strip(Strip) = "strip";
1149		/// The action bar.
1150		actions(Vec<PickerAction>) = "actions";
1151	}
1152}
1153
1154/// A picker sheet.
1155pub fn picker<M>() -> Picker<M> {
1156	new("picker")
1157}
1158
1159// ---------------------------------------------------------------------------
1160// Progress and motion
1161// ---------------------------------------------------------------------------
1162
1163common_kind!(
1164	/// `spinner`: an activity indicator.
1165	Spinner
1166);
1167
1168impl<M> Spinner<M> {
1169	props! {
1170		/// The indicator.
1171		style(SpinnerStyle) = "style";
1172		/// Text after it.
1173		label(Text) = "label";
1174	}
1175}
1176
1177/// An activity indicator.
1178pub fn spinner<M>() -> Spinner<M> {
1179	new("spinner")
1180}
1181
1182common_kind!(
1183	/// `shimmer`: a shimmering label.
1184	Shimmer
1185);
1186
1187impl<M> Shimmer<M> {
1188	props! {
1189		/// How it sweeps.
1190		mode(ShimmerMode) = "mode";
1191		/// The sweep colors.
1192		palette(ShimmerPalette) = "palette";
1193	}
1194
1195	/// Replaces the text (plain or spans).
1196	pub fn text(mut self, text: impl Into<Text>) -> Self {
1197		self.0.set_text(text.into());
1198		self
1199	}
1200}
1201
1202/// A shimmering label.
1203pub fn shimmer<M>(text: impl Into<Text>) -> Shimmer<M> {
1204	new::<M, Shimmer<M>>("shimmer").text(text)
1205}
1206
1207common_kind!(
1208	/// `elapsed`: a live timer Tern clocks.
1209	Elapsed
1210);
1211
1212impl<M> Elapsed<M> {
1213	props! {
1214		/// Milliseconds already elapsed (negative: a countdown).
1215		age(i64) = "age";
1216		/// Freezes the display at this many ms.
1217		stopped(u64) = "stopped";
1218		/// How the time reads.
1219		format(ElapsedFormat) = "format";
1220	}
1221
1222	/// Sets `age` from when the timed work started.
1223	pub fn since(self, start: Instant) -> Self {
1224		let age = i64::try_from(start.elapsed().as_millis()).unwrap_or(i64::MAX);
1225		self.age(age)
1226	}
1227}
1228
1229/// A live timer.
1230pub fn elapsed<M>() -> Elapsed<M> {
1231	new("elapsed")
1232}
1233
1234common_kind!(
1235	/// `rate`: a number easing between updates.
1236	Rate
1237);
1238
1239impl<M> Rate<M> {
1240	props! {
1241		/// The target value.
1242		value(f64) = "value";
1243		/// Appended after a space.
1244		unit(String) = "unit";
1245	}
1246}
1247
1248/// An easing number.
1249pub fn rate<M>(value: f64) -> Rate<M> {
1250	new::<M, Rate<M>>("rate").value(value)
1251}
1252
1253common_kind!(
1254	/// `progress`: a progress bar (indeterminate without a value).
1255	Progress
1256);
1257
1258impl<M> Progress<M> {
1259	props! {
1260		/// Fraction done, 0–1.
1261		value(f64) = "value";
1262		/// Text after the bar.
1263		label(Text) = "label";
1264	}
1265}
1266
1267/// A progress bar.
1268pub fn progress<M>() -> Progress<M> {
1269	new("progress")
1270}
1271
1272common_kind!(
1273	/// `meter`: a value as a bar, ring or grid.
1274	Meter
1275);
1276
1277impl<M> Meter<M> {
1278	props! {
1279		/// The level, 0–1.
1280		value(f64) = "value";
1281		/// Stacked parts.
1282		parts(Vec<Part>) = "parts";
1283		/// Tint levels.
1284		thresholds(Thresholds) = "thresholds";
1285		/// The shape.
1286		style(MeterStyle) = "style";
1287		/// The size.
1288		size(Size) = "size";
1289		/// `blocks`: one row of this many cells.
1290		steps(u32) = "steps";
1291		/// Ticks.
1292		marks(Vec<MeterMark>) = "marks";
1293		/// Text after the shape.
1294		label(Text) = "label";
1295		/// Text after the label.
1296		total(Text) = "total";
1297	}
1298}
1299
1300/// A meter.
1301pub fn meter<M>() -> Meter<M> {
1302	new("meter")
1303}
1304
1305kind_type!(
1306	/// `chart`: bars, a sparkline, a heatmap, a line or area chart or a
1307	/// donut.
1308	///
1309	/// Its `min`/`max` are a number value domain, not the common size
1310	/// bounds.
1311	Chart
1312);
1313
1314impl<M> Chart<M> {
1315	base_methods!();
1316
1317	title_method!();
1318
1319	actions_methods!();
1320
1321	event_methods!();
1322
1323	props! {
1324		/// Which chart.
1325		kind(ChartKind) = "kind";
1326		/// The size.
1327		size(Size) = "size";
1328		/// The fill color token (the first series' color).
1329		token(String) = "token";
1330		/// A line under the chart.
1331		summary(Text) = "summary";
1332		/// `bars`/`spark`: one bar per entry; `line`/`area`: one point per
1333		/// entry; `donut`: one segment per entry.
1334		series(Vec<Point>) = "series";
1335		/// `line`/`area`: several series, one line each.
1336		multi_series(Vec<Series>) = "series";
1337		/// `line`/`area`: the x labels by point index.
1338		labels(Vec<String>) = "labels";
1339		/// `line`/`area`/`donut`: written with every value printed: a currency
1340		/// symbol (`$`, `€`, `£`, `¥`) leads it, `%`, `×` and `°` attach after it,
1341		/// words follow a space.
1342		unit(String) = "unit";
1343		/// `line`/`area` with one series: the label beside the last point
1344		/// (`true` its value, text replaces it, `false` hides it).
1345		last(TextFlag) = "last";
1346		/// `donut`: what the ring's middle says (default the total).
1347		total(Text) = "total";
1348		/// `donut`: a quiet line under the ring.
1349		caption(Text) = "caption";
1350		/// `bars`/`spark`/`line`/`area`: the value domain's low end (default
1351		/// 0); values are clamped into it.
1352		min(f64) = "min";
1353		/// `bars`/`spark`/`line`/`area`: the value domain's high end
1354		/// (default the largest series or mark value).
1355		max(f64) = "max";
1356		/// `bars`/`spark`/`line`/`area`: dashed reference lines in value
1357		/// units.
1358		marks(Vec<ChartMark>) = "marks";
1359		/// `heatmap`: rows of intensities 0–1.
1360		cells(Vec<Vec<Option<f64>>>) = "cells";
1361		/// `heatmap`: a tooltip per cell.
1362		tips(Vec<Vec<Option<String>>>) = "tips";
1363		/// `heatmap`: a label per row.
1364		rows(Vec<String>) = "rows";
1365		/// `heatmap`: column labels.
1366		cols(Vec<ChartCol>) = "cols";
1367	}
1368}
1369
1370/// A chart.
1371pub fn chart<M>(kind: impl Into<ChartKind>) -> Chart<M> {
1372	new::<M, Chart<M>>("chart").kind(kind)
1373}
1374
1375common_kind!(
1376	/// `effort`: a thinking-effort glyph.
1377	Effort
1378);
1379
1380impl<M> Effort<M> {
1381	props! {
1382		/// The rung.
1383		level(EffortLevel) = "level";
1384	}
1385}
1386
1387/// A thinking-effort glyph.
1388pub fn effort<M>(level: impl Into<EffortLevel>) -> Effort<M> {
1389	new::<M, Effort<M>>("effort").level(level)
1390}
1391
1392// ---------------------------------------------------------------------------
1393// Inputs
1394// ---------------------------------------------------------------------------
1395
1396common_kind!(
1397	/// `editor`: a multi-line text field whose text the program owns.
1398	Editor
1399);
1400
1401impl<M> Editor<M> {
1402	props! {
1403		/// The text.
1404		text(String) = "text";
1405		/// The caret, UTF-16.
1406		cursor(usize) = "cursor";
1407		/// The selection anchor, UTF-16.
1408		anchor(usize) = "anchor";
1409		/// Styled ranges.
1410		decor(Vec<Decor>) = "decor";
1411		/// An inline completion after the caret.
1412		ghost(String) = "ghost";
1413		/// Dim text while empty.
1414		placeholder(Text) = "placeholder";
1415		/// Spans before the first line.
1416		prompt(Text) = "prompt";
1417		/// A vim mode label.
1418		mode(String) = "mode";
1419		/// The text is code in this language.
1420		lang(String) = "lang";
1421		/// Not editable.
1422		readonly(bool) = "readonly";
1423		/// Accepts an atomic `send`.
1424		sendable(bool) = "sendable";
1425		/// Input cannot be read yet: Tern keeps what is typed until this clears.
1426		typeahead(bool) = "typeahead";
1427		/// Cap the height at this many lines.
1428		max_lines(u32) = "maxLines";
1429		/// The look (`field` frames it as a prompt input).
1430		variant(InputVariant) = "variant";
1431		/// Buttons attached inside the ring, in order.
1432		buttons(Vec<ActButton>) = "buttons";
1433	}
1434}
1435
1436/// A multi-line text field.
1437pub fn editor<M>() -> Editor<M> {
1438	new("editor")
1439}
1440
1441common_kind!(
1442	/// `input`: a single-line text field.
1443	Input
1444);
1445
1446impl<M> Input<M> {
1447	props! {
1448		/// The text.
1449		text(String) = "text";
1450		/// The caret, UTF-16.
1451		cursor(usize) = "cursor";
1452		/// The selection anchor, UTF-16.
1453		anchor(usize) = "anchor";
1454		/// Styled ranges.
1455		decor(Vec<Decor>) = "decor";
1456		/// An inline completion after the caret.
1457		ghost(String) = "ghost";
1458		/// Dim text while empty.
1459		placeholder(Text) = "placeholder";
1460		/// Spans before the text.
1461		prompt(Text) = "prompt";
1462		/// A vim mode label.
1463		mode(String) = "mode";
1464		/// The text is code in this language.
1465		lang(String) = "lang";
1466		/// Not editable.
1467		readonly(bool) = "readonly";
1468		/// Accepts an atomic `send`.
1469		sendable(bool) = "sendable";
1470		/// Input cannot be read yet: Tern keeps what is typed until this clears.
1471		typeahead(bool) = "typeahead";
1472		/// The look (default `field` with any addon, else `well`).
1473		variant(InputVariant) = "variant";
1474		/// A leading icon.
1475		icon(Icon) = "icon";
1476		/// In a sans region, keep the mono face for the text.
1477		mono(bool) = "mono";
1478		/// A clear × while there is text; sends `action` `clear`.
1479		clear(bool) = "clear";
1480		/// A mono well before the text.
1481		prefix(String) = "prefix";
1482		/// A mono well after the text.
1483		suffix(String) = "suffix";
1484		/// A mono unit after the text, inside the field.
1485		unit(String) = "unit";
1486		/// An attached ⌃⌄ segment; sends `action` `inc` / `dec`.
1487		stepper(bool) = "stepper";
1488		/// Draws a `•` per character, with an eye that reveals it.
1489		secret(bool) = "secret";
1490		/// A secret's shown state.
1491		reveal(bool) = "reveal";
1492		/// Buttons attached to the ring, in order.
1493		buttons(Vec<ActButton>) = "buttons";
1494	}
1495
1496	/// The keycap of the chord that reaches the field, shown while it is
1497	/// idle and empty.
1498	pub fn keys<K: Into<String>>(mut self, keys: impl IntoIterator<Item = K>) -> Self {
1499		self
1500			.0
1501			.set("keys", &keys.into_iter().map(Into::into).collect::<Vec<String>>());
1502		self
1503	}
1504}
1505
1506/// A single-line text field.
1507pub fn input<M>() -> Input<M> {
1508	new("input")
1509}
1510
1511// ---------------------------------------------------------------------------
1512// Bars, toasts and overlays
1513// ---------------------------------------------------------------------------
1514
1515container_kind!(
1516	/// `status`: a status bar strip of `seg`s.
1517	StatusBar
1518);
1519
1520impl<M> StatusBar<M> {
1521	props! {
1522		/// No fill.
1523		transparent(bool) = "transparent";
1524	}
1525}
1526
1527/// A status bar strip.
1528pub fn status<M>() -> StatusBar<M> {
1529	new("status")
1530}
1531
1532container_kind!(
1533	/// `seg`: a status bar segment.
1534	Seg
1535);
1536
1537impl<M> Seg<M> {
1538	props! {
1539		/// An icon.
1540		icon(Icon) = "icon";
1541		/// The right group.
1542		side(Side) = "side";
1543		/// Drop order: lower drops first.
1544		priority(f64) = "priority";
1545	}
1546
1547	/// Replaces the text (plain or spans).
1548	pub fn text(mut self, text: impl Into<Text>) -> Self {
1549		self.0.set_text(text.into());
1550		self
1551	}
1552}
1553
1554/// A status bar segment.
1555pub fn seg<M>(text: impl Into<Text>) -> Seg<M> {
1556	new::<M, Seg<M>>("seg").text(text)
1557}
1558
1559common_kind!(
1560	/// `toast`: a transient notice.
1561	Toast
1562);
1563
1564impl<M> Toast<M> {
1565	props! {
1566		/// The message.
1567		text(String) = "text";
1568		/// A mono detail.
1569		#[allow(clippy::should_implement_trait, reason = "the prop is named `sub`")]
1570		sub(String) = "sub";
1571		/// Take it down after this many ms.
1572		ttl(u64) = "ttl";
1573	}
1574}
1575
1576/// A transient notice.
1577pub fn toast<M>(text: impl Into<String>) -> Toast<M> {
1578	new::<M, Toast<M>>("toast").text(text)
1579}
1580
1581container_kind!(
1582	/// `overlay`: a floating panel or sheet in `layer`.
1583	Overlay
1584);
1585
1586impl<M> Overlay<M> {
1587	props! {
1588		/// Placement.
1589		anchor(Anchor) = "anchor";
1590		/// Card width or full sheet; with `side`, the sheet's width or the
1591		/// drawer's height.
1592		size(OverlaySize) = "size";
1593		/// A dim backdrop taking the pointer.
1594		modal(bool) = "modal";
1595		/// The title row.
1596		head(Text) = "head";
1597		/// Docks it to an edge: a full-height sheet or a bottom drawer (wins
1598		/// over `anchor`).
1599		side(OverlaySide) = "side";
1600		/// With `side`: flush with the edge or inset from it.
1601		variant(OverlayVariant) = "variant";
1602	}
1603
1604	panel_head_methods!();
1605
1606	/// With `side`: the sheet's width or the drawer's height as a fraction
1607	/// of the layer, in (0, 1].
1608	pub fn size_fraction(mut self, fraction: f64) -> Self {
1609		self.0.set("size", &fraction);
1610		self
1611	}
1612
1613	/// A node drawn in a sticky footer bar under the body (the sheet's
1614	/// actions).
1615	pub fn foot(mut self, foot: impl Into<Slot<M>>) -> Self {
1616		set_slot(&mut self.0, "foot", foot.into());
1617		self
1618	}
1619}
1620
1621/// A floating panel.
1622pub fn overlay<M>() -> Overlay<M> {
1623	new("overlay")
1624}
1625
1626common_kind!(
1627	/// `button`: a push button. A click sends `action` with
1628	/// `actions.click` (`"click"` by default); it never takes focus.
1629	Button
1630);
1631
1632impl<M> Button<M> {
1633	props! {
1634		/// The label.
1635		text(String) = "text";
1636		/// A leading icon.
1637		icon(Icon) = "icon";
1638		/// The chrome.
1639		variant(ButtonVariant) = "variant";
1640		/// The height.
1641		size(ButtonSize) = "size";
1642		/// Drawn pressed (a toggle that is on).
1643		on(bool) = "on";
1644		/// Dimmed and silent; a reason string is the tooltip.
1645		disabled(Disabled) = "disabled";
1646		/// A spinner leads and it sends nothing.
1647		busy(bool) = "busy";
1648	}
1649
1650	/// Keycaps drawn after the label (display only).
1651	pub fn keys<K: Into<String>>(mut self, keys: impl IntoIterator<Item = K>) -> Self {
1652		self
1653			.0
1654			.set("keys", &keys.into_iter().map(Into::into).collect::<Vec<String>>());
1655		self
1656	}
1657}
1658
1659/// A push button labeled `text`.
1660pub fn button<M>(text: impl Into<String>) -> Button<M> {
1661	new::<M, Button<M>>("button").text(text)
1662}
1663
1664common_kind!(
1665	/// `stats`: a well of stat tiles.
1666	Stats
1667);
1668
1669impl<M> Stats<M> {
1670	props! {
1671		/// All tiles.
1672		items(Vec<StatItem>) = "items";
1673	}
1674
1675	/// Adds a tile.
1676	pub fn item(mut self, item: StatItem) -> Self {
1677		push(&mut self.0, "items", &item);
1678		self
1679	}
1680}
1681
1682/// A well of stat tiles.
1683pub fn stats<M>(items: impl IntoIterator<Item = StatItem>) -> Stats<M> {
1684	new::<M, Stats<M>>("stats").items(items.into_iter().collect::<Vec<_>>())
1685}
1686
1687// ---------------------------------------------------------------------------
1688// Form controls
1689// ---------------------------------------------------------------------------
1690
1691common_kind!(
1692	/// `select`: a value picked from a menu, as a field or a quiet trigger,
1693	/// optionally searchable.
1694	///
1695	/// A pick sends `change` with the option's `value`; the pointer opening
1696	/// or closing it sends `toggle` with `open`.
1697	Select
1698);
1699
1700impl<M> Select<M> {
1701	props! {
1702		/// The current option's value.
1703		value(String) = "value";
1704		/// The options, in order.
1705		options(Vec<SelectOption>) = "options";
1706		/// The look.
1707		variant(SelectVariant) = "variant";
1708		/// A muted label before the value on the trigger.
1709		label(String) = "label";
1710		/// The trigger's text without a value (and the search line's
1711		/// without a query).
1712		placeholder(String) = "placeholder";
1713		/// The height.
1714		size(Size) = "size";
1715		/// Dimmed and inert; a reason string is the tooltip.
1716		disabled(Disabled) = "disabled";
1717		/// The menu shows.
1718		open(bool) = "open";
1719		/// The highlighted option's value (your keyboard cursor).
1720		active(String) = "active";
1721		/// A combobox: a search line over the options.
1722		search(bool) = "search";
1723		/// The search text.
1724		query(String) = "query";
1725		/// The caret in `query`, UTF-16.
1726		cursor(usize) = "cursor";
1727		/// The size of the whole set, counted beside the options shown.
1728		total(u64) = "total";
1729		/// The menu's text when there are no options.
1730		empty(String) = "empty";
1731	}
1732
1733	/// Adds an option.
1734	pub fn option(mut self, option: impl Into<SelectOption>) -> Self {
1735		push(&mut self.0, "options", &option.into());
1736		self
1737	}
1738}
1739
1740/// A value picked from a menu.
1741pub fn select<M>() -> Select<M> {
1742	new("select")
1743}
1744
1745common_kind!(
1746	/// `switch`: an on/off setting. A click flips it at once and sends
1747	/// `change` with the new state as `value`.
1748	Switch
1749);
1750
1751impl<M> Switch<M> {
1752	props! {
1753		/// Whether it is on.
1754		on(bool) = "on";
1755		/// The look.
1756		variant(SwitchVariant) = "variant";
1757		/// The setting's name (a labeled switch fills its row).
1758		label(String) = "label";
1759		/// A muted line under the label.
1760		hint(String) = "hint";
1761		/// Dimmed and inert; a reason string is the tooltip.
1762		disabled(Disabled) = "disabled";
1763	}
1764
1765	/// The words of a `segment`'s two sides, off first.
1766	pub fn labels(mut self, off: impl Into<String>, on: impl Into<String>) -> Self {
1767		let labels: [String; 2] = [off.into(), on.into()];
1768		self.0.set("labels", &labels);
1769		self
1770	}
1771}
1772
1773/// An on/off switch labeled `label`.
1774pub fn switch<M>(label: impl Into<String>) -> Switch<M> {
1775	new::<M, Switch<M>>("switch").label(label)
1776}
1777
1778kind_type!(
1779	/// `slider`: a value, or a range, dragged along a track.
1780	///
1781	/// Releasing a drag that moved it sends `change` with the value (a pair
1782	/// for a range). Its `min`/`max` are the track's ends, not the common
1783	/// size bounds.
1784	Slider
1785);
1786
1787impl<M> Slider<M> {
1788	base_methods!();
1789
1790	title_method!();
1791
1792	actions_methods!();
1793
1794	event_methods!();
1795
1796	props! {
1797		/// The value, or a range of two.
1798		value(SliderValue) = "value";
1799		/// The low end (default 0).
1800		min(f64) = "min";
1801		/// The high end (default 100).
1802		max(f64) = "max";
1803		/// The distance between values, counted from `min`.
1804		step(f64) = "step";
1805		/// The name at the left.
1806		label(String) = "label";
1807		/// Written after the number.
1808		unit(String) = "unit";
1809		/// How the value reads.
1810		format(SliderFormat) = "format";
1811		/// Tick marks under the track.
1812		ticks(Ticks) = "ticks";
1813		/// The look.
1814		variant(SliderVariant) = "variant";
1815		/// Dimmed and inert; a reason string is the tooltip.
1816		disabled(Disabled) = "disabled";
1817	}
1818}
1819
1820/// A slider.
1821pub fn slider<M>() -> Slider<M> {
1822	new("slider")
1823}
1824
1825container_kind!(
1826	/// `field`: a labelled form row around its control(s): label, hint,
1827	/// error or warning.
1828	Field
1829);
1830
1831impl<M> Field<M> {
1832	props! {
1833		/// The field's name.
1834		label(Text) = "label";
1835		/// The help line.
1836		hint(Text) = "hint";
1837		/// Something is wrong: the note in `--bad` (`true` rings the
1838		/// control without a line).
1839		error(TextFlag) = "error";
1840		/// Worth a look: the note in `--warn`.
1841		warning(TextFlag) = "warning";
1842		/// A `*` after the label (display only).
1843		required(bool) = "required";
1844		/// `grid`: the label column's width.
1845		label_width(Extent) = "labelWidth";
1846		/// The layout.
1847		variant(FieldVariant) = "variant";
1848	}
1849}
1850
1851/// A form row labeled `label`.
1852pub fn field<M>(label: impl Into<Text>) -> Field<M> {
1853	new::<M, Field<M>>("field").label(label)
1854}
1855
1856common_kind!(
1857	/// `otp`: a one-time code, one cell per character.
1858	///
1859	/// Your program owns the keys; a click on a cell sends `action` `focus`
1860	/// with the cell as `value`.
1861	Otp
1862);
1863
1864impl<M> Otp<M> {
1865	props! {
1866		/// The characters typed so far.
1867		text(String) = "text";
1868		/// The number of cells (1–12).
1869		length(u32) = "length";
1870		/// Cells per group (`0`: one group).
1871		group(u32) = "group";
1872		/// The look.
1873		variant(OtpVariant) = "variant";
1874		/// Hides what was typed.
1875		mask(OtpMask) = "mask";
1876		/// `error` or `success`.
1877		state(OtpState) = "state";
1878		/// A quiet line under the cells.
1879		hint(String) = "hint";
1880		/// Dimmed and inert; a reason string is the tooltip.
1881		disabled(Disabled) = "disabled";
1882	}
1883
1884	/// The active cell (0-based; default after the text); `None` shows
1885	/// none.
1886	pub fn cursor(mut self, cursor: Option<u32>) -> Self {
1887		match cursor {
1888			Some(cell) => self.0.set("cursor", &cell),
1889			None => self.0.set("cursor", &false),
1890		}
1891		self
1892	}
1893}
1894
1895/// A one-time code of `length` cells.
1896pub fn otp<M>(length: u32) -> Otp<M> {
1897	new::<M, Otp<M>>("otp").length(length)
1898}
1899
1900container_kind!(
1901	/// `buttons`: `button` children drawn as one group: joined, split or a
1902	/// toolbar.
1903	Buttons
1904);
1905
1906impl<M> Buttons<M> {
1907	props! {
1908		/// The look.
1909		variant(ButtonsVariant) = "variant";
1910		/// The children's size.
1911		size(ButtonSize) = "size";
1912		/// The group's accessible name.
1913		label(String) = "label";
1914	}
1915}
1916
1917/// A group of buttons.
1918pub fn buttons<M>() -> Buttons<M> {
1919	new("buttons")
1920}
1921
1922kind_type!(
1923	/// `calendar`: a month grid or a week strip of days.
1924	///
1925	/// A pick sends `change` with the day (a `[from, to]` range with
1926	/// `range`), a preset `select`, a month step `action` `month`. Its
1927	/// `min`/`max` are days, not the common size bounds.
1928	Calendar
1929);
1930
1931impl<M> Calendar<M> {
1932	base_methods!();
1933
1934	title_method!();
1935
1936	actions_methods!();
1937
1938	event_methods!();
1939
1940	props! {
1941		/// The look.
1942		variant(CalendarVariant) = "variant";
1943		/// The selection: a day, or a range.
1944		value(DateValue) = "value";
1945		/// Pick a range instead of a day.
1946		range(bool) = "range";
1947		/// The month shown, `YYYY-MM`.
1948		month(String) = "month";
1949		/// The day ringed as today, `YYYY-MM-DD`.
1950		today(String) = "today";
1951		/// The first day that can be picked.
1952		min(String) = "min";
1953		/// The last day that can be picked.
1954		max(String) = "max";
1955		/// The first column of the month grid.
1956		week_start(WeekStart) = "weekStart";
1957		/// Days carrying a dot.
1958		marks(Vec<DayMark>) = "marks";
1959		/// The day your keys are on.
1960		cursor(String) = "cursor";
1961		/// `week`: the presets column.
1962		presets(Vec<Preset>) = "presets";
1963		/// The active preset's id.
1964		preset(String) = "preset";
1965		/// A muted line under the days.
1966		note(String) = "note";
1967	}
1968}
1969
1970/// A calendar.
1971pub fn calendar<M>() -> Calendar<M> {
1972	new("calendar")
1973}
1974
1975kind_type!(
1976	/// `date`: a date field that opens a calendar popover.
1977	///
1978	/// It sends the calendar's events, and `toggle` when a click opens or
1979	/// closes it. Its `min`/`max` are days, not the common size bounds.
1980	Date
1981);
1982
1983impl<M> Date<M> {
1984	base_methods!();
1985
1986	title_method!();
1987
1988	actions_methods!();
1989
1990	event_methods!();
1991
1992	props! {
1993		/// The selection: a day, or a range.
1994		value(DateValue) = "value";
1995		/// Pick a range; the popover stays open until its second end.
1996		range(bool) = "range";
1997		/// The popover shows.
1998		open(bool) = "open";
1999		/// Shown in the field without a value.
2000		placeholder(String) = "placeholder";
2001		/// The popover's month, `YYYY-MM`.
2002		month(String) = "month";
2003		/// The day ringed as today, `YYYY-MM-DD`.
2004		today(String) = "today";
2005		/// The first day that can be picked.
2006		min(String) = "min";
2007		/// The last day that can be picked.
2008		max(String) = "max";
2009		/// The first column of the month grid.
2010		week_start(WeekStart) = "weekStart";
2011		/// Days carrying a dot.
2012		marks(Vec<DayMark>) = "marks";
2013		/// The day your keys are on.
2014		cursor(String) = "cursor";
2015		/// A muted line under the days.
2016		note(String) = "note";
2017	}
2018}
2019
2020/// A date field.
2021pub fn date<M>() -> Date<M> {
2022	new("date")
2023}
2024
2025// ---------------------------------------------------------------------------
2026// Menus and navigation
2027// ---------------------------------------------------------------------------
2028
2029common_kind!(
2030	/// `menu`: a button that opens a menu of commands.
2031	///
2032	/// Choosing a row sends `action` with its id as `act` (and `checked` for
2033	/// check and radio rows); the pointer opening or closing it sends
2034	/// `toggle` with `open`.
2035	Menu
2036);
2037
2038impl<M> Menu<M> {
2039	props! {
2040		/// The rows.
2041		items(Vec<MenuEntry>) = "items";
2042		/// The trigger's label (without it and `icon`, a … button).
2043		text(String) = "text";
2044		/// The trigger's icon.
2045		icon(Icon) = "icon";
2046		/// The trigger's chrome.
2047		trigger(ButtonVariant) = "trigger";
2048		/// A chevron after the label.
2049		caret(bool) = "caret";
2050		/// The trigger's height.
2051		size(ButtonSize) = "size";
2052		/// The menu's look.
2053		variant(MenuVariant) = "variant";
2054		/// The menu shows.
2055		open(bool) = "open";
2056		/// The highlighted row's id (your keyboard cursor).
2057		active(String) = "active";
2058		/// The trigger edge the menu lines up with.
2059		align(MenuAlign) = "align";
2060		/// The trigger is dimmed and does not open; a reason string is the
2061		/// tooltip.
2062		disabled(Disabled) = "disabled";
2063	}
2064
2065	/// Adds a row.
2066	pub fn item(mut self, entry: impl Into<MenuEntry>) -> Self {
2067		push(&mut self.0, "items", &entry.into());
2068		self
2069	}
2070}
2071
2072/// A menu button.
2073pub fn menu<M>() -> Menu<M> {
2074	new("menu")
2075}
2076
2077common_kind!(
2078	/// `menubar`: a strip of menus.
2079	///
2080	/// Choosing a row sends `action` with the menu as `item`; the pointer
2081	/// opening or closing a menu sends `toggle` with `item` and `open`.
2082	Menubar
2083);
2084
2085impl<M> Menubar<M> {
2086	props! {
2087		/// The menus, in order.
2088		menus(Vec<MenubarMenu>) = "menus";
2089		/// The look.
2090		variant(MenubarVariant) = "variant";
2091		/// The app's mark before the titles.
2092		icon(Icon) = "icon";
2093		/// The id of the menu shown open.
2094		open(String) = "open";
2095		/// The highlighted row's id in the open menu.
2096		active(String) = "active";
2097	}
2098
2099	/// Adds a menu.
2100	pub fn menu(mut self, menu: MenubarMenu) -> Self {
2101		push(&mut self.0, "menus", &menu);
2102		self
2103	}
2104}
2105
2106/// A strip of menus.
2107pub fn menubar<M>() -> Menubar<M> {
2108	new("menubar")
2109}
2110
2111common_kind!(
2112	/// `nav`: navigation links, some opening a menu or panel of sub-items.
2113	/// A pick sends `select` with the item.
2114	Nav
2115);
2116
2117impl<M> Nav<M> {
2118	props! {
2119		/// The items, in order.
2120		items(Vec<NavItem>) = "items";
2121		/// The active item's or sub-item's id.
2122		active(String) = "active";
2123		/// The id of the item whose menu or panel shows.
2124		open(String) = "open";
2125		/// The look.
2126		variant(NavVariant) = "variant";
2127	}
2128
2129	/// Adds an item.
2130	pub fn item(mut self, item: NavItem) -> Self {
2131		push(&mut self.0, "items", &item);
2132		self
2133	}
2134}
2135
2136/// Navigation links.
2137pub fn nav<M>() -> Nav<M> {
2138	new("nav")
2139}
2140
2141kind_type!(
2142	/// `breadcrumb`: a trail of places, the last one current.
2143	///
2144	/// A click on a crumb sends `select` with its id. Its `max` is a crumb
2145	/// count, not the common size bounds.
2146	Breadcrumb
2147);
2148
2149impl<M> Breadcrumb<M> {
2150	base_methods!();
2151
2152	min_method!();
2153
2154	title_method!();
2155
2156	actions_methods!();
2157
2158	event_methods!();
2159
2160	props! {
2161		/// The trail, root first.
2162		items(Vec<Crumb>) = "items";
2163		/// The look.
2164		variant(BreadcrumbVariant) = "variant";
2165		/// Most crumbs shown; the rest fold into a "…" menu.
2166		max(u32) = "max";
2167		/// The fold's menu shows.
2168		open(bool) = "open";
2169		/// The id of the crumb under your keyboard cursor.
2170		active(String) = "active";
2171	}
2172
2173	/// Adds a crumb.
2174	pub fn crumb(mut self, crumb: Crumb) -> Self {
2175		push(&mut self.0, "items", &crumb);
2176		self
2177	}
2178}
2179
2180/// A trail of places.
2181pub fn breadcrumb<M>() -> Breadcrumb<M> {
2182	new("breadcrumb")
2183}
2184
2185common_kind!(
2186	/// `pager`: pagination. A page click sends `change` with the page; the
2187	/// `more` row sends `action` `more`.
2188	Pager
2189);
2190
2191impl<M> Pager<M> {
2192	props! {
2193		/// The look.
2194		variant(PagerVariant) = "variant";
2195		/// The current page, 1-based (`more`: the pages loaded).
2196		page(u64) = "page";
2197		/// The page count (default from `total` / `size`).
2198		pages(u64) = "pages";
2199		/// The item count.
2200		total(u64) = "total";
2201		/// Items per page.
2202		size(u64) = "size";
2203		/// A turning ring; the more row sends nothing.
2204		loading(bool) = "loading";
2205		/// What the items are, plural.
2206		label(String) = "label";
2207	}
2208}
2209
2210/// Pagination.
2211pub fn pager<M>() -> Pager<M> {
2212	new("pager")
2213}
2214
2215// ---------------------------------------------------------------------------
2216// Containers
2217// ---------------------------------------------------------------------------
2218
2219kind_type!(
2220	/// `split`: panes (its children) the user resizes by dragging the
2221	/// handles between them.
2222	///
2223	/// A release sends `change` with the shares. Its `min` is the panes'
2224	/// minimums (or the common bound).
2225	Split
2226);
2227
2228impl<M> Split<M> {
2229	base_methods!();
2230
2231	title_method!();
2232
2233	max_method!();
2234
2235	actions_methods!();
2236
2237	event_methods!();
2238
2239	children_methods!();
2240
2241	props! {
2242		/// The look.
2243		variant(SplitVariant) = "variant";
2244		/// Side by side or stacked.
2245		dir(SplitDir) = "dir";
2246		/// Each pane's share, in child order.
2247		sizes(Vec<f64>) = "sizes";
2248		/// The panes' minimums along the split.
2249		min(PaneMin) = "min";
2250		/// The split's height.
2251		h(Extent) = "h";
2252		/// The handle your keys move (0 is after the first pane).
2253		active(u32) = "active";
2254	}
2255}
2256
2257/// Resizable panes.
2258pub fn split<M>() -> Split<M> {
2259	new("split")
2260}
2261
2262container_kind!(
2263	/// `scroll`: a scrolling container, optionally pinned to its end.
2264	/// Scrolling is the viewer's; it sends nothing.
2265	Scroll
2266);
2267
2268impl<M> Scroll<M> {
2269	props! {
2270		/// The look.
2271		variant(ScrollVariant) = "variant";
2272		/// Keep to the end while content grows.
2273		stick(Stick) = "stick";
2274		/// A stuck view follows its end.
2275		pinned(bool) = "pinned";
2276		/// When the thumb shows.
2277		bar(ScrollBar) = "bar";
2278		/// Space between the children.
2279		gap(Gap) = "gap";
2280		/// What the counts name.
2281		noun(Noun) = "noun";
2282	}
2283}
2284
2285/// A scrolling container.
2286pub fn scroll<M>() -> Scroll<M> {
2287	new("scroll")
2288}
2289
2290container_kind!(
2291	/// `carousel`: slides (its children) shown one at a time. A move by
2292	/// the user sends `change` with the index shown.
2293	Carousel
2294);
2295
2296impl<M> Carousel<M> {
2297	props! {
2298		/// The slide shown, from 0.
2299		index(u32) = "index";
2300		/// Moving past either end wraps around.
2301		looping(bool) = "loop";
2302		/// The look.
2303		variant(CarouselVariant) = "variant";
2304		/// The slides' height (default 200px).
2305		h(Height) = "h";
2306	}
2307}
2308
2309/// Slides shown one at a time.
2310pub fn carousel<M>() -> Carousel<M> {
2311	new("carousel")
2312}
2313
2314// ---------------------------------------------------------------------------
2315// People and placeholders
2316// ---------------------------------------------------------------------------
2317
2318common_kind!(
2319	/// `avatar`: a person, bot or org as initials, an icon or a picture.
2320	Avatar
2321);
2322
2323impl<M> Avatar<M> {
2324	props! {
2325		/// The shape.
2326		variant(AvatarVariant) = "variant";
2327		/// The name: its first two characters are the initials.
2328		text(String) = "text";
2329		/// Picks the colors (default `text`).
2330		seed(String) = "seed";
2331		/// An icon drawn instead of the initials.
2332		icon(Icon) = "icon";
2333		/// A picture's blob id.
2334		blob(String) = "blob";
2335		/// The size.
2336		size(AvatarSize) = "size";
2337		/// A status dot in the tone's color.
2338		status(Tone) = "status";
2339	}
2340}
2341
2342/// An avatar of `text`.
2343pub fn avatar<M>(text: impl Into<String>) -> Avatar<M> {
2344	new::<M, Avatar<M>>("avatar").text(text)
2345}
2346
2347kind_type!(
2348	/// `avatars`: avatars stacked, with an overflow count. Its `max` is an
2349	/// avatar count, not the common size bounds.
2350	Avatars
2351);
2352
2353impl<M> Avatars<M> {
2354	base_methods!();
2355
2356	min_method!();
2357
2358	title_method!();
2359
2360	actions_methods!();
2361
2362	event_methods!();
2363
2364	props! {
2365		/// The avatars, front first.
2366		items(Vec<AvatarItem>) = "items";
2367		/// How many show before the `+N` chip.
2368		max(u32) = "max";
2369		/// Every avatar's size.
2370		size(AvatarSize) = "size";
2371		/// Muted text after the stack, and its accessible name.
2372		label(String) = "label";
2373	}
2374}
2375
2376/// A stack of avatars.
2377pub fn avatars<M>(items: impl IntoIterator<Item = AvatarItem>) -> Avatars<M> {
2378	new::<M, Avatars<M>>("avatars").items(items.into_iter().collect::<Vec<_>>())
2379}
2380
2381kind_type!(
2382	/// `empty`: an empty state: icon, title, a line and its children (the
2383	/// actions). Its `title` is text, not a tooltip.
2384	Empty
2385);
2386
2387impl<M> Empty<M> {
2388	base_methods!();
2389
2390	min_method!();
2391
2392	max_method!();
2393
2394	actions_methods!();
2395
2396	event_methods!();
2397
2398	children_methods!();
2399
2400	props! {
2401		/// The look.
2402		variant(EmptyVariant) = "variant";
2403		/// The icon (`""` for none).
2404		icon(Icon) = "icon";
2405		/// What is missing, in a few words.
2406		title(Text) = "title";
2407		/// One muted line: why, or what to do.
2408		text(Text) = "text";
2409		/// `inline`: a hairline card around the row.
2410		frame(bool) = "frame";
2411		/// `drop`: a drag is over the zone.
2412		over(bool) = "over";
2413	}
2414}
2415
2416/// An empty state titled `title`.
2417pub fn empty<M>(title: impl Into<Text>) -> Empty<M> {
2418	new::<M, Empty<M>>("empty").title(title)
2419}
2420
2421common_kind!(
2422	/// `skeleton`: placeholder shapes while content loads.
2423	Skeleton
2424);
2425
2426impl<M> Skeleton<M> {
2427	props! {
2428		/// The look.
2429		variant(SkeletonVariant) = "variant";
2430		/// What it stands for.
2431		shape(SkeletonShape) = "shape";
2432		/// `text`: how many lines.
2433		lines(u32) = "lines";
2434		/// `rows`: how many rows.
2435		rows(u32) = "rows";
2436		/// `card`: how many stat tiles.
2437		tiles(u32) = "tiles";
2438	}
2439}
2440
2441/// Placeholder shapes.
2442pub fn skeleton<M>() -> Skeleton<M> {
2443	new("skeleton")
2444}
2445
2446// ---------------------------------------------------------------------------
2447// Tool, agent and task kinds
2448// ---------------------------------------------------------------------------
2449
2450kind_type!(
2451	/// `tool`: one coding-agent tool call.
2452	Tool
2453);
2454
2455impl<M> Tool<M> {
2456	base_methods!();
2457
2458	min_method!();
2459
2460	max_method!();
2461
2462	actions_methods!();
2463
2464	event_methods!();
2465
2466	children_methods!();
2467
2468	props! {
2469		/// The tool's name (picks the icon).
2470		name(String) = "name";
2471		/// The verb.
2472		title(Text) = "title";
2473		/// The primary argument.
2474		target(Text) = "target";
2475		/// How a string target is drawn.
2476		target_kind(TargetKind) = "targetKind";
2477		/// Short facts after the target.
2478		meta(Vec<Text>) = "meta";
2479		/// Chips in the head.
2480		badges(Vec<BadgeSpec>) = "badges";
2481		/// A short state note.
2482		note(Text) = "note";
2483		/// A non-zero exit code.
2484		exit(i64) = "exit";
2485		/// The state (`ask` waits for approval).
2486		status(Status) = "status";
2487		/// The approval look while `status` is `ask`.
2488		confirm(ToolConfirm) = "confirm";
2489		/// The approval panel's contents. Its buttons send `action` `allow`,
2490		/// `deny` or `always` with the selected scope as `value`.
2491		ask(Ask) = "ask";
2492		/// The resolved answer, shown as a head chip.
2493		approved(Approved) = "approved";
2494		/// Elapsed ms when sent.
2495		age(i64) = "age";
2496		/// The final duration in ms.
2497		took(u64) = "took";
2498		/// The head's tooltip.
2499		intent(String) = "intent";
2500		/// Ring or no ring.
2501		frame(ToolFrame) = "frame";
2502		/// The head toggles the body.
2503		collapsible(bool) = "collapsible";
2504		/// The initial fold.
2505		collapsed(bool) = "collapsed";
2506		/// The clamp of the collapsed body.
2507		preview(ToolPreview) = "preview";
2508		/// Hover buttons.
2509		tools(Vec<ToolButton>) = "tools";
2510	}
2511}
2512
2513/// A tool call.
2514pub fn tool<M>(name: impl Into<String>) -> Tool<M> {
2515	new::<M, Tool<M>>("tool").name(name)
2516}
2517
2518container_kind!(
2519	/// `agent`: one subagent row.
2520	Agent
2521);
2522
2523impl<M> Agent<M> {
2524	props! {
2525		/// The display name.
2526		name(String) = "name";
2527		/// The agent type.
2528		agent(String) = "agent";
2529		/// More chips.
2530		badges(Vec<BadgeSpec>) = "badges";
2531		/// The one-line task.
2532		task(Text) = "task";
2533		/// The state.
2534		status(AgentStatus) = "status";
2535		/// The model.
2536		model(String) = "model";
2537		/// The model dot's palette token.
2538		thinking(String) = "thinking";
2539		/// Stats.
2540		stats(AgentStats) = "stats";
2541		/// The tool running now.
2542		tool(AgentTool) = "tool";
2543		/// A retry countdown.
2544		retry(Retry) = "retry";
2545		/// Nesting depth.
2546		depth(u32) = "depth";
2547		/// The row toggles the children.
2548		collapsible(bool) = "collapsible";
2549		/// The initial fold.
2550		collapsed(bool) = "collapsed";
2551	}
2552}
2553
2554/// A subagent row.
2555pub fn agent<M>(name: impl Into<String>) -> Agent<M> {
2556	new::<M, Agent<M>>("agent").name(name)
2557}
2558
2559common_kind!(
2560	/// `checklist`: a todo list.
2561	Checklist
2562);
2563
2564impl<M> Checklist<M> {
2565	props! {
2566		/// The phases.
2567		phases(Vec<Phase>) = "phases";
2568		/// The presentation.
2569		mode(ChecklistMode) = "mode";
2570		/// `reminder`: text after the count.
2571		note(Text) = "note";
2572	}
2573}
2574
2575/// A todo list.
2576pub fn checklist<M>() -> Checklist<M> {
2577	new("checklist")
2578}
2579
2580kind_type!(
2581	/// `prefs`: a settings page or sheet.
2582	Prefs
2583);
2584
2585impl<M> Prefs<M> {
2586	base_methods!();
2587
2588	min_method!();
2589
2590	max_method!();
2591
2592	actions_methods!();
2593
2594	event_methods!();
2595
2596	children_methods!();
2597
2598	props! {
2599		/// The brand title.
2600		title(String) = "title";
2601		/// Nav rows.
2602		pages(Vec<PrefsPage>) = "pages";
2603		/// The current page.
2604		page(String) = "page";
2605		/// A paragraph under the page title.
2606		lead(String) = "lead";
2607		/// The sections.
2608		sections(Vec<PrefsSection>) = "sections";
2609		/// Search text.
2610		query(String) = "query";
2611		/// The caret in the search field, UTF-16.
2612		cursor(usize) = "cursor";
2613		/// The row or section with the focus ring.
2614		focus(String) = "focus";
2615		/// The row being edited.
2616		editing(Editing) = "editing";
2617	}
2618}
2619
2620/// A settings page.
2621pub fn prefs<M>() -> Prefs<M> {
2622	new("prefs")
2623}
2624
2625// ---------------------------------------------------------------------------
2626// Chat and agent work
2627// ---------------------------------------------------------------------------
2628
2629kind_type!(
2630	/// `message`: one chat message: author, body (its children), reactions
2631	/// and actions.
2632	///
2633	/// A reaction flip sends `change` with the chip's text and `on`; an
2634	/// action button sends `action`. Its `actions` are icon buttons, not the
2635	/// common pointer actions.
2636	Message
2637);
2638
2639impl<M> Message<M> {
2640	base_methods!();
2641
2642	min_method!();
2643
2644	title_method!();
2645
2646	max_method!();
2647
2648	event_methods!();
2649
2650	children_methods!();
2651
2652	props! {
2653		/// The look.
2654		variant(MessageVariant) = "variant";
2655		/// `end` is the local user.
2656		side(MessageSide) = "side";
2657		/// The name in the header.
2658		author(String) = "author";
2659		/// The time in the header.
2660		time(String) = "time";
2661		/// Same author as the message before: no header, no avatar.
2662		continued(bool) = "continued";
2663		/// Reaction chips.
2664		reactions(Vec<Reaction>) = "reactions";
2665		/// Icon buttons shown on hover.
2666		actions(Vec<IconAction>) = "actions";
2667		/// When the actions show.
2668		reveal(Reveal) = "reveal";
2669		/// A quiet line under the message.
2670		status(String) = "status";
2671		/// Still on its way: the status shimmers.
2672		pending(bool) = "pending";
2673	}
2674
2675	/// The avatar: an `avatar`, `icon` or `image` node drawn in the avatar
2676	/// slot (or the id of a child).
2677	pub fn avatar(mut self, avatar: impl Into<Slot<M>>) -> Self {
2678		set_slot(&mut self.0, "avatar", avatar.into());
2679		self
2680	}
2681}
2682
2683/// A chat message.
2684pub fn message<M>() -> Message<M> {
2685	new("message")
2686}
2687
2688kind_type!(
2689	/// `attachment`: a file attached to a message, with its upload state.
2690	///
2691	/// Its buttons send `action` (`remove`, `retry` or one of `actions`).
2692	/// Its `actions` are icon buttons, not the common pointer actions.
2693	Attachment
2694);
2695
2696impl<M> Attachment<M> {
2697	base_methods!();
2698
2699	min_method!();
2700
2701	title_method!();
2702
2703	max_method!();
2704
2705	event_methods!();
2706
2707	props! {
2708		/// The file name.
2709		name(String) = "name";
2710		/// The look.
2711		variant(AttachmentVariant) = "variant";
2712		/// The extension shown (default from `name`).
2713		ext(String) = "ext";
2714		/// The MIME type.
2715		mime(String) = "mime";
2716		/// Size in bytes.
2717		size(u64) = "size";
2718		/// The meta line at rest.
2719		meta(String) = "meta";
2720		/// An image blob id: the thumbnail.
2721		blob(String) = "blob";
2722		/// A file opened by a click when there is no `href`.
2723		path(String) = "path";
2724		/// The upload state.
2725		state(AttachmentState) = "state";
2726		/// 0–1 while busy.
2727		progress(f64) = "progress";
2728		/// The meta line in the `error` state.
2729		error(String) = "error";
2730		/// A × that sends `remove`.
2731		removable(bool) = "removable";
2732		/// Action icons before the ×.
2733		actions(Vec<IconAction>) = "actions";
2734	}
2735}
2736
2737/// An attached file named `name`.
2738pub fn attachment<M>(name: impl Into<String>) -> Attachment<M> {
2739	new::<M, Attachment<M>>("attachment").name(name)
2740}
2741
2742kind_type!(
2743	/// `trace`: an agent's steps, each opening in place.
2744	///
2745	/// A child keyed by a line's id is that line's content; the rest is the
2746	/// body. Flipping a line or the head sends `toggle` with `open` (and the
2747	/// line as `item`). Its `title` is text, not a tooltip.
2748	Trace
2749);
2750
2751impl<M> Trace<M> {
2752	base_methods!();
2753
2754	min_method!();
2755
2756	max_method!();
2757
2758	actions_methods!();
2759
2760	event_methods!();
2761
2762	children_methods!();
2763
2764	props! {
2765		/// The look.
2766		variant(TraceVariant) = "variant";
2767		/// The steps, in order.
2768		lines(Vec<TraceLine>) = "lines";
2769		/// The whole trace's status.
2770		status(Status) = "status";
2771		/// The head text.
2772		title(Text) = "title";
2773		/// The `summary` head's text (default derived).
2774		summary(Text) = "summary";
2775		/// The head glyph.
2776		icon(Icon) = "icon";
2777		/// Elapsed ms when sent.
2778		age(u64) = "age";
2779		/// The total time in ms once settled.
2780		took(u64) = "took";
2781		/// Whether the head's fold is open.
2782		open(bool) = "open";
2783		/// The `Sources` row at the foot.
2784		sources(Vec<Source>) = "sources";
2785	}
2786
2787	/// Adds a step.
2788	pub fn line(mut self, line: TraceLine) -> Self {
2789		push(&mut self.0, "lines", &line);
2790		self
2791	}
2792}
2793
2794/// An agent's steps.
2795pub fn trace<M>() -> Trace<M> {
2796	new("trace")
2797}
2798
2799common_kind!(
2800	/// `stacktrace`: an error and its stack frames.
2801	///
2802	/// A click on a frame sends `select`, a double click `activate`, with
2803	/// the frame's index as `item`.
2804	Stacktrace
2805);
2806
2807impl<M> Stacktrace<M> {
2808	props! {
2809		/// The error.
2810		error(StackError) = "error";
2811		/// The stack, innermost first.
2812		frames(Vec<Frame>) = "frames";
2813		/// The selected frame's index.
2814		selected(u32) = "selected";
2815		/// The excerpts' language.
2816		lang(String) = "lang";
2817		/// The look.
2818		variant(StacktraceVariant) = "variant";
2819	}
2820
2821	/// Adds a frame.
2822	pub fn frame(mut self, frame: Frame) -> Self {
2823		push(&mut self.0, "frames", &frame);
2824		self
2825	}
2826}
2827
2828/// An error and its stack.
2829pub fn stacktrace<M>(error: StackError) -> Stacktrace<M> {
2830	new::<M, Stacktrace<M>>("stacktrace").error(error)
2831}
2832
2833common_kind!(
2834	/// `suggestions`: suggested prompts. A click sends `activate` with the
2835	/// item's id (else its text).
2836	Suggestions
2837);
2838
2839impl<M> Suggestions<M> {
2840	props! {
2841		/// The look.
2842		variant(SuggestionsVariant) = "variant";
2843		/// The prompts, in order.
2844		items(Vec<Suggestion>) = "items";
2845	}
2846
2847	/// Adds a prompt.
2848	pub fn item(mut self, item: Suggestion) -> Self {
2849		push(&mut self.0, "items", &item);
2850		self
2851	}
2852}
2853
2854/// Suggested prompts.
2855pub fn suggestions<M>() -> Suggestions<M> {
2856	new("suggestions")
2857}
2858
2859container_kind!(
2860	/// `questions`: a questionnaire.
2861	///
2862	/// A choice sends `change` with the question as `item`; Next, Back and
2863	/// Skip send `action` with the question left as `item`; Submit sends
2864	/// `action` `submit` with every answer as `value`. An `input` child keyed
2865	/// `<id>.other` takes a question's free-text answer.
2866	Questions
2867);
2868
2869impl<M> Questions<M> {
2870	props! {
2871		/// The questions.
2872		items(Vec<Question>) = "items";
2873		/// The current question's index.
2874		step(u32) = "step";
2875		/// The answers by question id.
2876		answers(std::collections::BTreeMap<String, QuestionAnswer>) = "answers";
2877		/// The look.
2878		variant(QuestionsVariant) = "variant";
2879		/// `inline`'s hint line (`""` hides it).
2880		hint(String) = "hint";
2881		/// The submit label.
2882		submit(String) = "submit";
2883	}
2884
2885	/// Adds a question.
2886	pub fn question(mut self, question: Question) -> Self {
2887		push(&mut self.0, "items", &question);
2888		self
2889	}
2890}
2891
2892/// A questionnaire.
2893pub fn questions<M>() -> Questions<M> {
2894	new("questions")
2895}
2896
2897/// Appends `value` to the list prop `key`.
2898fn push<M>(node: &mut Node<M>, key: &str, value: &impl serde::Serialize) {
2899	let Ok(value) = serde_json::to_value(value) else {
2900		return;
2901	};
2902	match node.props.get_mut(key) {
2903		Some(serde_json::Value::Array(list)) => list.push(value),
2904		_ => {
2905			node
2906				.props
2907				.insert(key.to_owned(), serde_json::Value::Array(vec![value]));
2908		},
2909	}
2910}
2911
2912#[cfg(test)]
2913mod tests {
2914	use serde_json::json;
2915
2916	use super::*;
2917
2918	fn wire<M>(node: impl Into<Node<M>>) -> serde_json::Value {
2919		let node = node.into();
2920		json!({ "k": node.kind, "p": node.props })
2921	}
2922
2923	#[test]
2924	fn text_takes_a_string_or_spans() {
2925		assert_eq!(wire::<()>(text("hi")), json!({"k": "text", "p": {"text": "hi"}}));
2926		let styled = text::<()>(spans!["a", span("b").style(Token::Muted).style("strong")]);
2927		assert_eq!(
2928			wire(styled.clone()),
2929			json!({"k": "text", "p": {"spans": [{"t": "a"}, {"t": "b", "s": "muted strong"}]}})
2930		);
2931		// Switching back to plain text drops the spans.
2932		assert_eq!(wire(styled.text("c")), json!({"k": "text", "p": {"text": "c"}}));
2933	}
2934
2935	#[test]
2936	fn json_carries_its_value_and_open_levels() {
2937		assert_eq!(
2938			wire::<()>(super::json(json!({"a": 1})).open(1)),
2939			json!({"k": "json", "p": {"value": {"a": 1}, "open": 1}})
2940		);
2941	}
2942
2943	#[test]
2944	fn typed_values_and_escape_hatches_serialize() {
2945		let c = card::<()>()
2946			.head("Deploy")
2947			.status(Status::Running)
2948			.tone("brand-new")
2949			.min(Bound::w(Extent::Ch(10.0)))
2950			.basis(Basis::Content)
2951			.key(7)
2952			.prop("future", json!([1]))
2953			.prop("head", serde_json::Value::Null);
2954		assert_eq!(
2955			wire(c),
2956			json!({"k": "card", "p": {
2957				"status": "running", "tone": "brand-new", "min": {"w": "10ch"},
2958				"basis": "content", "key": 7, "future": [1]
2959			}})
2960		);
2961	}
2962
2963	#[test]
2964	fn code_bars_folds_and_overview_serialize() {
2965		let c = code::<()>("a\nb\nc")
2966			.marks(vec![CodeMark { line: 1, wash: vec![0.9, 0.3], ..CodeMark::default() }])
2967			.folds(vec![
2968				CodeFold {
2969					from: 2,
2970					to: 3,
2971					note: Some("cut".into()),
2972					tone: Some(Tone::Error),
2973					hovercard: Some(Node::<()>::from(text("why")).to_json()),
2974					..CodeFold::default()
2975				},
2976				CodeFold { from: 5, to: 6, ..CodeFold::default() },
2977			])
2978			.overview(true)
2979			.nums(vec![1, 2, 7]);
2980		assert_eq!(
2981			wire(c),
2982			json!({"k": "code", "p": {
2983				"text": "a\nb\nc", "nums": [1, 2, 7], "marks": [{"line": 1, "wash": [0.9, 0.3]}],
2984				"folds": [
2985					{"from": 2, "to": 3, "note": "cut", "tone": "error", "hovercard": {"k": "text", "p": {"text": "why"}}},
2986					{"from": 5, "to": 6}
2987				],
2988				"overview": true
2989			}})
2990		);
2991	}
2992
2993	#[test]
2994	fn kind_specific_props_replace_common_meanings() {
2995		let title = spans![span("Heading").style("strong")];
2996		assert_eq!(
2997			wire(tool::<()>("read").title(title.clone()))["p"]["title"],
2998			json!([{"t": "Heading", "s": "strong"}])
2999		);
3000		assert_eq!(
3001			wire(picker::<()>().title(title).actions(vec![PickerAction {
3002				id: "open".into(),
3003				primary: Some(true),
3004				..PickerAction::default()
3005			}]))["p"],
3006			json!({
3007				"title": [{"t": "Heading", "s": "strong"}],
3008				"actions": [{"id": "open", "primary": true}]
3009			})
3010		);
3011		assert_eq!(wire(prefs::<()>().title("Settings"))["p"]["title"], "Settings");
3012		assert_eq!(
3013			wire(list::<()>().max_fraction(0.5).max_lines(4))["p"]["max"],
3014			json!({"lines": 4})
3015		);
3016		assert_eq!(wire(list::<()>().max_lines(4).max_fraction(0.5))["p"]["max"], 0.5);
3017	}
3018
3019	#[test]
3020	fn agent_statuses_have_their_own_vocabulary() {
3021		for (status, expected) in [
3022			(AgentStatus::Pending, "pending"),
3023			(AgentStatus::Running, "running"),
3024			(AgentStatus::Done, "done"),
3025			(AgentStatus::Failed, "failed"),
3026			(AgentStatus::Aborted, "aborted"),
3027			(AgentStatus::Idle, "idle"),
3028			(AgentStatus::Parked, "parked"),
3029			(AgentStatus::Other("future".into()), "future"),
3030		] {
3031			assert_eq!(wire(agent::<()>("worker").status(status))["p"]["status"], expected);
3032		}
3033	}
3034
3035	#[test]
3036	fn table_meter_cells_keep_the_wire_wrapper() {
3037		let node = wire(table::<()>().row(TableRow::new("a").cell("usage", MeterCell {
3038			value: Some(0.5),
3039			title: Some("Half".into()),
3040			tone: Some(Tone::Success),
3041			..MeterCell::default()
3042		})));
3043		assert_eq!(
3044			node["p"]["rows"][0]["cells"]["usage"],
3045			json!({
3046				"meter": {"value": 0.5, "title": "Half", "tone": "success"}
3047			})
3048		);
3049		assert_eq!(
3050			serde_json::to_value(Cell::Meter(MeterCell::default())).unwrap(),
3051			json!({"meter": {}})
3052		);
3053	}
3054
3055	#[test]
3056	fn form_controls_serialize_their_props() {
3057		assert_eq!(
3058			wire(
3059				select::<()>()
3060					.label("Model")
3061					.value("haiku")
3062					.option("opus")
3063					.option(
3064						SelectOption::new("haiku", "Haiku")
3065							.detail("200k")
3066							.group("Fast")
3067					)
3068					.search(true)
3069			)["p"],
3070			json!({
3071				"label": "Model", "value": "haiku", "search": true, "options": [
3072					{"value": "opus"},
3073					{"value": "haiku", "label": "Haiku", "detail": "200k", "group": "Fast"}
3074				]
3075			})
3076		);
3077		assert_eq!(
3078			wire(
3079				switch::<()>("Wrap")
3080					.on(true)
3081					.variant(SwitchVariant::Segment)
3082					.labels("Off", "On")
3083			),
3084			json!({"k": "switch", "p": {
3085				"label": "Wrap", "on": true, "variant": "segment", "labels": ["Off", "On"]
3086			}})
3087		);
3088		assert_eq!(
3089			wire(
3090				slider::<()>()
3091					.value((200.0, 1200.0))
3092					.min(0.0)
3093					.max(2000.0)
3094					.ticks(4u32)
3095					.unit("ms")
3096			)["p"],
3097			json!({"value": [200.0, 1200.0], "min": 0.0, "max": 2000.0, "ticks": 4, "unit": "ms"})
3098		);
3099		assert_eq!(
3100			wire(
3101				field::<()>("Name")
3102					.required(true)
3103					.error(true)
3104					.label_width(Extent::Ch(14.0))
3105					.child(input())
3106			)["p"],
3107			json!({"label": "Name", "required": true, "error": true, "labelWidth": "14ch"})
3108		);
3109		assert_eq!(
3110			wire(otp::<()>(6).text("12").mask("*").cursor(None))["p"],
3111			json!({"length": 6, "text": "12", "mask": "*", "cursor": false})
3112		);
3113		assert_eq!(
3114			wire(
3115				calendar::<()>()
3116					.range(true)
3117					.value(DateValue::Range("2026-10-19".into(), None))
3118					.week_start(WeekStart::Sun)
3119					.marks(vec![DayMark::from("2026-10-20"), DayMark::new("2026-10-21", Tone::Error)])
3120			)["p"],
3121			json!({
3122				"range": true, "value": ["2026-10-19", null], "weekStart": "sun",
3123				"marks": [{"date": "2026-10-20"}, {"date": "2026-10-21", "tone": "error"}]
3124			})
3125		);
3126	}
3127
3128	#[test]
3129	fn menu_entries_serialize_as_strings_headers_and_items() {
3130		let entries = vec![
3131			MenuEntry::from("open"),
3132			MenuEntry::header("Danger"),
3133			MenuEntry::Separator,
3134			MenuItem::new("trash", "Move to Trash")
3135				.icon("trash")
3136				.keys(["cmd", "backspace"])
3137				.danger(true)
3138				.into(),
3139			MenuItem::new("export", "Export")
3140				.item(MenuItem::new("csv", "CSV").checked(true))
3141				.into(),
3142		];
3143		let expected = json!([
3144			"open", {"header": "Danger"}, "-",
3145			{"id": "trash", "label": "Move to Trash", "icon": "trash",
3146				"keys": ["cmd", "backspace"], "danger": true},
3147			{"id": "export", "label": "Export",
3148				"items": [{"id": "csv", "label": "CSV", "checked": true}]}
3149		]);
3150		assert_eq!(wire(menu::<()>().text("More").items(entries.clone()))["p"]["items"], expected);
3151		let actions = entries
3152			.into_iter()
3153			.fold(Actions::new(), |actions, entry| actions.menu(entry));
3154		assert_eq!(wire(text::<()>("log").actions(actions))["p"]["actions"]["menu"], expected);
3155	}
3156
3157	#[test]
3158	fn kinds_with_their_own_min_max_title_and_actions_keep_them() {
3159		assert_eq!(wire(avatars::<()>([AvatarItem::new("Cara")]).max(4u32))["p"]["max"], 4);
3160		assert_eq!(wire(breadcrumb::<()>().crumb(Crumb::new("src", "src")).max(3u32))["p"]["max"], 3);
3161		assert_eq!(
3162			wire(
3163				split::<()>()
3164					.min(vec![Some(Extent::Ch(20.0)), None])
3165					.sizes(vec![2.0, 1.0])
3166			)["p"],
3167			json!({"min": ["20ch", null], "sizes": [2.0, 1.0]})
3168		);
3169		assert_eq!(
3170			wire(empty::<()>(spans![span("No runs").style("strong")]))["p"]["title"],
3171			json!([{"t": "No runs", "s": "strong"}])
3172		);
3173		assert_eq!(
3174			wire(attachment::<()>("a.png").actions(vec![IconAction::new("open").icon("eye")]))["p"],
3175			json!({"name": "a.png", "actions": [{"act": "open", "icon": "eye"}]})
3176		);
3177		assert_eq!(
3178			wire(carousel::<()>().looping(true).h(Height::Lines(10.0)))["p"],
3179			json!({"loop": true, "h": "10lines"})
3180		);
3181	}
3182
3183	#[test]
3184	fn work_kinds_serialize_lines_frames_and_questions() {
3185		let t = trace::<()>().status(Status::Running).line(
3186			TraceLine::new("s1", "Searched")
3187				.target("tern tsp")
3188				.status(Status::Done)
3189				.took(1200)
3190				.source(Source::new("https://x.dev").site("x")),
3191		);
3192		assert_eq!(
3193			wire(t)["p"],
3194			json!({"status": "running", "lines": [{
3195				"id": "s1", "title": "Searched", "target": "tern tsp", "status": "done",
3196				"took": 1200, "sources": [{"href": "https://x.dev", "site": "x"}]
3197			}]})
3198		);
3199		let s = stacktrace::<()>(StackError::new("TypeError", "x is undefined"))
3200			.frame(Frame::new("main", "src/a.ts").at(3, 7).app(false));
3201		assert_eq!(
3202			wire(s)["p"],
3203			json!({
3204				"error": {"type": "TypeError", "message": "x is undefined"},
3205				"frames": [{"fn": "main", "file": "src/a.ts", "line": 3, "col": 7, "app": false}]
3206			})
3207		);
3208		let q = questions::<()>()
3209			.question(
3210				Question::new("db", "Which database?")
3211					.choice("postgres")
3212					.choice(QuestionChoice::new("sqlite", "SQLite"))
3213					.other("Something else"),
3214			)
3215			.answers(std::collections::BTreeMap::from([(
3216				"db".to_owned(),
3217				QuestionAnswer::from("postgres"),
3218			)]));
3219		assert_eq!(
3220			wire(q)["p"],
3221			json!({
3222				"items": [{"id": "db", "prompt": "Which database?",
3223					"choices": [{"value": "postgres"}, {"value": "sqlite", "label": "SQLite"}],
3224					"other": {"placeholder": "Something else"}}],
3225				"answers": {"db": "postgres"}
3226			})
3227		);
3228		let m = message::<()>()
3229			.author("Cara")
3230			.side(MessageSide::End)
3231			.reactions(vec![Reaction::new("👍", 2).on(true)])
3232			.avatar(avatar("Cara"))
3233			.child(md("hi"));
3234		let node: Node<()> = m.into();
3235		assert_eq!(node.props()["reactions"], json!([{"text": "👍", "count": 2, "on": true}]));
3236		assert_eq!(node.child_nodes()[0].props()["key"], "avatar");
3237		assert_eq!(node.child_nodes()[1].kind(), "md");
3238	}
3239
3240	#[test]
3241	fn extended_kinds_take_their_new_props() {
3242		let t = table::<()>()
3243			.col(Column::new("name", "Name").sortable(true))
3244			.sort(Sort::desc("name"))
3245			.multi(true)
3246			.picked(vec!["a".to_owned()])
3247			.bar_button(ActButton::new("restart", "Restart").icon("refresh"));
3248		assert_eq!(
3249			wire(t)["p"],
3250			json!({
3251				"cols": [{"id": "name", "head": "Name", "sortable": true}],
3252				"sort": {"col": "name", "dir": "desc"}, "multi": true, "picked": ["a"],
3253				"bar": [{"act": "restart", "text": "Restart", "icon": "refresh"}]
3254			})
3255		);
3256		assert_eq!(
3257			wire(
3258				chart::<()>(ChartKind::Line)
3259					.multi_series(vec![Series::new(vec![Some(1.0), None]).label("p50")])
3260					.unit("ms")
3261					.last(false)
3262			)["p"],
3263			json!({
3264				"kind": "line", "series": [{"label": "p50", "values": [1.0, null]}],
3265				"unit": "ms", "last": false
3266			})
3267		);
3268		assert_eq!(
3269			wire(
3270				input::<()>()
3271					.icon("search")
3272					.keys(["cmd", "f"])
3273					.clear(true)
3274					.secret(true)
3275			)["p"],
3276			json!({"icon": "search", "keys": ["cmd", "f"], "clear": true, "secret": true})
3277		);
3278		assert_eq!(
3279			wire(overlay::<()>().side(OverlaySide::Right).size_fraction(0.4))["p"],
3280			json!({"side": "right", "size": 0.4})
3281		);
3282		assert_eq!(
3283			wire(
3284				tool::<()>("bash")
3285					.status(Status::Ask)
3286					.confirm(ToolConfirm::Scopes)
3287					.ask(
3288						Ask::new("Run it?")
3289							.scope_option(AskScope::new("once", "Once"))
3290							.always(AskAlways::Hidden)
3291					)
3292			)["p"],
3293			json!({
3294				"name": "bash", "status": "ask", "confirm": "scopes",
3295				"ask": {"text": "Run it?", "always": false, "scopes": [{"id": "once", "label": "Once"}]}
3296			})
3297		);
3298		assert_eq!(
3299			wire(
3300				rule::<()>()
3301					.variant(RuleVariant::Note)
3302					.label("Resumed")
3303					.time("14:02")
3304			)["p"],
3305			json!({"variant": "note", "label": "Resumed", "time": "14:02"})
3306		);
3307		let cited = text::<()>(spans![
3308			span("Rust 2024"),
3309			span("").cite(Cite::new("https://blog.rust-lang.org").n(2))
3310		])
3311		.cite(CiteStyle::Note);
3312		assert_eq!(
3313			wire(cited)["p"],
3314			json!({"cite": "note", "spans": [
3315				{"t": "Rust 2024"}, {"t": "", "cite": [{"href": "https://blog.rust-lang.org", "n": 2}]}
3316			]})
3317		);
3318	}
3319
3320	#[test]
3321	fn list_props_append() {
3322		let k = kv::<()>().item("a", "1").item("b", span("2").style("num"));
3323		assert_eq!(
3324			wire(k),
3325			json!({"k": "kv", "p": {"items": [
3326				{"k": "a", "v": "1"}, {"k": "b", "v": [{"t": "2", "s": "num"}]}
3327			]}})
3328		);
3329	}
3330}