#[non_exhaustive]pub struct El {Show 90 fields
pub kind: Kind,
pub style_profile: StyleProfile,
pub key: Option<String>,
pub block_pointer: bool,
pub hit_overflow: Sides,
pub focusable: bool,
pub focus_ring_placement: FocusRingPlacement,
pub always_show_focus_ring: bool,
pub selectable: bool,
pub selection_source: Option<SelectionSource>,
pub capture_keys: bool,
pub alpha_follows_focused_ancestor: bool,
pub blink_when_focused: bool,
pub state_follows_interactive_ancestor: bool,
pub hover_alpha: Option<HoverAlpha>,
pub source: Source,
pub axis: Axis,
pub gap: f32,
pub padding: Sides,
pub align: Align,
pub justify: Justify,
pub width: Size,
pub height: Size,
pub component_size: Option<ComponentSize>,
pub metrics_role: Option<MetricsRole>,
pub explicit_width: bool,
pub explicit_height: bool,
pub explicit_padding: bool,
pub explicit_gap: bool,
pub explicit_radius: bool,
pub explicit_font_family: bool,
pub explicit_mono_font_family: bool,
pub explicit_mono: bool,
pub fill: Option<Color>,
pub dim_fill: Option<Color>,
pub stroke: Option<Color>,
pub stroke_width: f32,
pub radius: Corners,
pub shadow: f32,
pub surface_role: SurfaceRole,
pub paint_overflow: Sides,
pub clip: bool,
pub scrollable: bool,
pub pin_end: bool,
pub arrow_nav_siblings: bool,
pub tooltip: Option<String>,
pub cursor: Option<Cursor>,
pub cursor_pressed: Option<Cursor>,
pub shader_override: Option<ShaderBinding>,
pub layout_override: Option<LayoutFn>,
pub virtual_items: Option<VirtualItems>,
pub scrollbar: bool,
pub text: Option<String>,
pub text_color: Option<Color>,
pub text_align: TextAlign,
pub text_wrap: TextWrap,
pub text_overflow: TextOverflow,
pub text_role: TextRole,
pub text_max_lines: Option<usize>,
pub font_size: f32,
pub line_height: f32,
pub font_family: FontFamily,
pub mono_font_family: FontFamily,
pub font_weight: FontWeight,
pub font_mono: bool,
pub text_italic: bool,
pub text_underline: bool,
pub text_strikethrough: bool,
pub text_link: Option<String>,
pub text_bg: Option<Color>,
pub math: Option<Arc<MathExpr>>,
pub math_display: MathDisplay,
pub icon: Option<IconSource>,
pub icon_stroke_width: f32,
pub image: Option<Image>,
pub image_tint: Option<Color>,
pub image_fit: ImageFit,
pub surface_source: Option<SurfaceSource>,
pub surface_alpha: SurfaceAlpha,
pub surface_fit: ImageFit,
pub surface_transform: Affine2,
pub vector_source: Option<Arc<VectorAsset>>,
pub vector_render_mode: VectorRenderMode,
pub children: Vec<El>,
pub opacity: f32,
pub translate: (f32, f32),
pub scale: f32,
pub animate: Option<Timing>,
pub redraw_within: Option<Duration>,
pub computed_id: String,
}Expand description
The core tree node.
Construct via the component builders (text, button, card,
column, …) and chain modifiers (.padding, .gap, .fill, …).
Avoid building El directly — the builders set polished defaults.
#[non_exhaustive] — El is meant to be built through the
component constructors, not by struct-literal syntax. Direct
construction from outside this crate is intentionally disabled
so adding new layout/style fields stays a non-breaking change.
Fields (Non-exhaustive)§
This struct is marked as non-exhaustive
Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.kind: Kind§style_profile: StyleProfile§key: Option<String>§block_pointer: bool§hit_overflow: SidesExpand this element’s pointer hit target beyond its transformed
layout rect. Layout-neutral and paint-neutral: siblings don’t
move, the element doesn’t draw larger, and focus rings / shadows
still use Self::paint_overflow.
Use sparingly for controls with deliberately small visuals but larger intended targets (resize handles, compact icon affordances, row chrome). Hover, press, cursor, tooltip, and click routing all share this expanded target, so the invisible area behaves like the visible control. Ancestor clips still bound hit-testing.
focusable: bool§focus_ring_placement: FocusRingPlacement§always_show_focus_ring: boolShow the focus ring on this node even when focus arrived via
pointer (i.e. the runtime’s focus_visible is false). Default
behavior matches the web platform’s :focus-visible heuristic
— ring on Tab, no ring on click. Widgets like text inputs and
text areas opt in here because the ring is a meaningful
“this surface is now the active editing target” affordance even
when activated by mouse, beyond what the caret alone shows.
selectable: boolWhen true, this node is a pointer target for the library’s
text-selection manager: pointer-down inside its rect starts (or
extends) the global crate::selection::Selection anchored at
this node’s key. The leaf must also carry an explicit
.key(...) — same convention as focusable widgets — so the
selection survives tree rebuilds.
Set via Self::selectable. Coordinates with focus on a
per-pointer-event basis: pointer-down on a focusable widget
transfers focus and clears selection; pointer-down on a
selectable-only leaf moves selection without disturbing focus.
selection_source: Option<SelectionSource>Optional source-backed selection payload. Plain text leaves
select/copy their rendered Self::text. Rich text systems can
attach a crate::selection::SelectionSource so pointer
positions resolve through rendered text but copy returns the
original driving syntax (for example Markdown or TeX).
capture_keys: boolWhen true, all key events (other than registered hotkeys) route
to this node as raw KeyDown instead of being interpreted by
the library’s defaults (Tab traversal, Enter/Space activation,
Escape escape). Used by text-input widgets that need to consume
Tab/Enter/etc. as text or editing actions. Implies focusable
at the runner — the flag only takes effect when the node is
also the focused target.
alpha_follows_focused_ancestor: boolWhen true, this node’s paint opacity is multiplied by the nearest focusable ancestor’s focus envelope (0..1). The library already animates that envelope on focus / blur; flagged nodes fade in and out with the same easing without any app-side focus tracking.
Used by text_input’s caret bar — the caret only paints when
the input is focused, fading via the standard focus animation.
Documented in widget_kit.md as part of the public surface.
blink_when_focused: boolWhen true, this node’s paint opacity is also multiplied by the
runtime’s caret blink alpha. Combine with
alpha_follows_focused_ancestor (the caret should blink only
while the input is focused) — the two compose multiplicatively.
Used by text_input / text_area’s caret bar.
state_follows_interactive_ancestor: boolWhen true, this node’s hover and press visual envelopes are borrowed from its nearest focusable ancestor instead of being driven by its own (always-zero) envelope.
The hit-test only ever resolves to a focusable target, so a child of an interactive container — a slider thumb, a select trigger’s chevron, the dot inside a radio — never receives hover or press envelopes of its own. Flagged children pick up the ancestor’s envelopes so they can lighten / darken / ring out alongside the surface that captured the input.
Used by slider’s thumb so grabbing the slider visibly
reacts on the thumb itself, mirroring shadcn’s
hover:ring-4 hover:ring-ring/50.
hover_alpha: Option<HoverAlpha>When Some, this node’s paint opacity is bound to the
subtree interaction envelope — max of the hover, focus,
and press envelopes for the subtree rooted here. The drawn
alpha interpolates from rest (no interaction anywhere in the
subtree) to peak (full interaction), then composes
multiplicatively with the existing Self::opacity /
inherited opacity stack.
“Interaction” includes hovering, pressing, or keyboard-focusing any descendant — so a hover-revealed close icon stays visible when its tab is keyboard-focused, and an action pill stays visible when the cursor moves to one of its focusable buttons. Mirrors CSS’s “this element OR any descendant is hot.”
Layout-neutral — the element’s geometry stays fixed regardless of interaction state. Use for hover-revealed close buttons, secondary actions on list rows, hover-only validation icons, and other “show on interaction” patterns whose visibility shouldn’t shift the surrounding layout.
source: Source§axis: Axis§gap: f32§padding: Sides§align: Align§justify: Justify§width: Size§height: Size§component_size: Option<ComponentSize>Optional t-shirt size for stock widgets. None means the active
theme supplies the component-class default.
metrics_role: Option<MetricsRole>Optional theme-facing metrics role. Stock widgets set this so the theme can resolve default height/padding/radius before layout; app-defined widgets can set the same role to opt into identical sizing behavior.
explicit_width: boolAuthor-overrode layout metrics. Stock constructors set defaults without these flags; public modifiers flip them so theme metrics do not clobber explicit app choices.
explicit_height: bool§explicit_padding: bool§explicit_gap: bool§explicit_radius: bool§explicit_font_family: bool§explicit_mono_font_family: boolAuthor overrode the monospace font face for this node — theme
application leaves Self::mono_font_family alone when set.
explicit_mono: boolAuthor opted this node into the monospace family via
Self::mono. Role modifiers (Self::caption, Self::label,
Self::body, Self::title, Self::heading,
Self::display) leave Self::font_mono alone when this flag
is set, so the natural reading order text(s).mono().caption()
keeps the mono family. Without this guard, role application
silently resets font_mono = false. The Self::code role
always forces font_mono = true regardless.
fill: Option<Color>§dim_fill: Option<Color>Alternate fill used when the nearest focusable ancestor’s focus
envelope is below 1.0; the painter linearly interpolates from
dim_fill toward fill as the envelope approaches 1.0. Used by
text_input / text_area selection bands so the highlight
remains visible (in a muted color) even when the input loses
focus, matching the macOS convention.
stroke: Option<Color>§stroke_width: f32§radius: CornersCorner radii in logical pixels. Authored as a scalar in the
common case (.radius(tokens::RADIUS_MD) works via
super::geometry::Corners::from); per-corner shapes use
super::geometry::Corners::top,
super::geometry::Corners::bottom, etc. The painter clamps each corner to
half the shorter side.
shadow: f32§surface_role: SurfaceRole§paint_overflow: SidesPermit this element to paint outside its layout bounds. The
outset enlarges the quad geometry handed to the shader (and
any focus / shadow / glow visuals are positioned in the
overflow band) while leaving the layout rect — and therefore
sibling positions and hit-testing — unchanged. Subject to
ancestor clip rects: a focused widget inside a clip()ped
parent has its overflow clipped, same as any other paint.
clip: boolClip this element’s own paint and descendants to its computed rect. Used by scroll panes, host-painted regions, overlays, and any region where overflow should not leak visually or receive events.
scrollable: boolThis element is a vertical scroll viewport. The layout pass reads
the offset from UiState’s scroll-offset side map keyed by
computed_id, clamps it to [0, content_h - viewport_h], and
writes the clamped value back. Set automatically by crate::scroll().
pin_end: boolWhen set on a Kind::Scroll container, the runtime tracks the
“stick to bottom” pin used by chat logs and activity feeds: the
scroll offset stays glued to the tail across content growth, the
user can scroll up to break the pin, and scrolling back to the
bottom re-engages it. No effect on non-scrollable nodes. Defaults
to false; opt in with Self::pin_end.
Mirrors egui’s ScrollArea::stick_to_bottom(true). The “is the
pin currently engaged” bit lives in
crate::state::UiState’s scroll subsystem, keyed by
computed_id; layout reads it each frame to decide whether to
snap the stored offset to max_offset before clamping.
Treat this element’s focusable children as a single arrow-navigable
group: while a focused element is one of the direct children,
Up / Down / Home / End move focus among the group’s
focusable siblings instead of being routed as a KeyDown. Tab
traversal is unchanged.
Used by popover_panel so menu items in a dropdown are
keyboard-navigable; available to any user widget that wants the
same semantics.
tooltip: Option<String>Tooltip text. When set, the runtime synthesizes a hover-driven
tooltip layer anchored to this node — appearing after the
hover delay elapses, fading in with the standard envelope, and
dismissed when the pointer leaves or presses the node. The
trigger doesn’t have to be focusable or keyed; the runtime
anchors the tooltip via the trigger’s computed_id.
cursor: Option<Cursor>Pointer cursor declared for this element. None falls through
to whatever an ancestor declared, else crate::cursor::Cursor::Default.
Resolution lives in crate::state::UiState::cursor: if a
press is captured, the cursor follows the press target;
otherwise the hovered node is walked root-ward for the first
explicit declaration. Disabled state is not auto-mapped —
widgets that want crate::cursor::Cursor::NotAllowed when disabled set it
explicitly in their build closure.
cursor_pressed: Option<Cursor>Cursor to show only while a press is captured at this exact
node. Powers the natural Grab → Grabbing transition: the
slider sets cursor=Grab + cursor_pressed=Grabbing, and the
resolver picks the latter while the press anchors here. Unlike
Self::cursor, this does not walk up: an ancestor’s
cursor_pressed doesn’t apply to a descendant press target.
The press target’s own cursor is the fallback when this is
None.
shader_override: Option<ShaderBinding>Override the implicit stock::rounded_rect binding for this
node’s surface. The escape hatch a user crate uses to bind a
custom shader (e.g. liquid_glass).
layout_override: Option<LayoutFn>Second escape hatch: author-supplied layout function that
positions this node’s direct children. When set, the layout
pass calls the function instead of running its column/row/
overlay distribution. The library still recurses into each
child and still drives hit-test / focus / animation / scroll
off the rects the function returns. See LayoutFn for the
contract.
virtual_items: Option<VirtualItems>Virtualized list state. Set by crate::virtual_list (and only
on Kind::VirtualList nodes). The layout pass uses this to
realize only the rows whose rect intersects the viewport. The
node is automatically scrollable + clip.
scrollbar: boolShow a draggable vertical scrollbar thumb when this node is
scrollable and its content overflows the viewport. The thumb
overlays the right edge of the viewport — it does not reflow
children. No effect on non-scrollable nodes. Defaults to
false; the crate::scroll() and crate::virtual_list()
constructors flip it on by default. Authors disable with
Self::no_scrollbar.
text: Option<String>§text_color: Option<Color>§text_align: TextAlign§text_wrap: TextWrap§text_overflow: TextOverflow§text_role: TextRole§text_max_lines: Option<usize>§font_size: f32§line_height: f32§font_family: FontFamily§mono_font_family: FontFamilyMonospace face used when Self::font_mono is set (or when the
node carries TextRole::Code). Stamped by theme application
from crate::Theme::mono_font_family unless the author set it
explicitly via Self::mono_font_family.
font_weight: FontWeight§font_mono: bool§text_italic: boolItalic styling. Author-set via Self::italic; honoured when
this El is a styled text leaf inside an Kind::Inlines parent
and (best-effort) on standalone text Els.
text_underline: boolUnderline styling. Author-set via Self::underline.
text_strikethrough: boolStrikethrough styling. Author-set via Self::strikethrough.
text_link: Option<String>Link target URL. When set on a text leaf inside Kind::Inlines,
the run renders as a link (themed) and runs sharing a URL group
together for hit-test. Author-set via Self::link.
text_bg: Option<Color>Inline-run background. When set on a text leaf inside
Kind::Inlines, the shaped span paints a solid quad behind
its glyphs (one rect per line if the span wraps). No effect on
standalone text Els — author wraps in a styled row() for
chip-shaped surfaces. Author-set via Self::background.
math: Option<Arc<MathExpr>>Native math expression rendered through Aetna’s math box layout.
Set by crate::tree::math, crate::tree::math_inline, and
crate::tree::math_block.
math_display: MathDisplay§icon: Option<IconSource>§icon_stroke_width: f32§image: Option<Image>Raster image. When set together with Kind::Image (or any
kind, though crate::image is the idiomatic builder) the
draw_ops pass emits a crate::ir::DrawOp::Image projected
per Self::image_fit and tinted by Self::image_tint.
Layout intrinsic is the image’s natural pixel size when both
width and height are Hug.
image_tint: Option<Color>Multiply each sampled pixel by this colour (RGBA [0..1]). Most
raster art wants None (no tint); set it for monochrome assets
(icon-style PNGs) the app wants to recolour.
image_fit: ImageFitHow the image projects into the resolved rect. Defaults to
ImageFit::Contain — preserves aspect ratio and letterboxes.
surface_source: Option<SurfaceSource>App-owned GPU texture source for Kind::Surface elements.
Set via Self::surface_source (typically through the
crate::tree::surface builder).
surface_alpha: SurfaceAlphaHow the surface texture composes with widgets painted below it.
Defaults to crate::surface::SurfaceAlpha::Premultiplied.
surface_fit: ImageFitHow the surface texture projects into the resolved rect.
Defaults to ImageFit::Fill — stretch to the rect, ignoring
aspect ratio. Contain / Cover / None mirror the
corresponding modes on crate::tree::image.
surface_transform: Affine2Affine applied to the texture quad in destination space, around
the centre of the post-fit rect. Defaults to identity.
Composes after Self::surface_fit: the fit projection picks
the destination rect, then this matrix transforms it (rotate,
scale, translate, shear). The auto-clip scissor still clamps
to the El’s content rect, so transforms that move the texture
outside that rect are cropped.
vector_source: Option<Arc<VectorAsset>>Vector asset for Kind::Vector elements. Set via
Self::vector_source (typically through the
crate::tree::vector builder). The asset’s view box determines
the natural aspect ratio.
vector_render_mode: VectorRenderModeRender policy for Self::vector_source. Defaults to
crate::vector::VectorRenderMode::Painted so authored vector
paint is preserved unless the caller explicitly opts into mask
rendering.
children: Vec<El>§opacity: f32Paint-time alpha multiplier in [0, 1]. Default 1.0. Multiplies
the alpha channel of fill, stroke, and text colour at draw
time. Layout-neutral. App-driven changes are eased when
Self::animate is set.
translate: (f32, f32)Paint-time offset in logical pixels. Default (0.0, 0.0).
Subtree-inheriting: descendants paint at their computed rect
plus all ancestor translate accumulated through the paint
recursion. Use this to slide a sidebar / drawer / list-item
without re-running layout. App-driven changes are eased when
Self::animate is set.
scale: f32Per-node uniform scale around the computed-rect centre. Default
1.0. Scales this node’s surface quad and (if it carries text)
its glyph run together. Not subtree-inheriting — descendants
keep their own scale. Use this for tap-bounce on a button. App-
driven changes are eased when Self::animate is set.
animate: Option<Timing>Opt-in app-driven prop interpolation. When Some(timing), the
animation tracker eases fill / text_color / stroke /
opacity / translate / scale between rebuilds — the value
the build closure produces becomes the spring/tween target;
current carries over from last frame. State visuals (hover /
press / focus ring) keep their own library defaults regardless.
redraw_within: Option<Duration>Inside-out redraw deadline: when Some(d) and this El is
visible (rect intersects the viewport), Aetna asks the host to
schedule the next frame within d. Aggregated across the tree
via min and surfaced as
[crate::runtime::PrepareResult::next_redraw_in]; the host
drives the loop, Aetna mediates by visibility.
Use this for any widget whose paint depends on time (animated
images, video frames written via surface(), custom shaders
that don’t go through the samples_time registration path,
hover-and-fade effects implemented outside the built-in
animation tracker). Duration::ZERO means “next frame ASAP”;
non-zero values let the host pace at lower-than-display
cadence.
computed_id: StringStable path-based ID, filled by the layout pass. Used as the
key for every side map that holds per-node bookkeeping in
crate::state::UiState — computed rects, interaction state,
state-envelope amounts, scroll offsets, in-flight animations.
Implementations§
Source§impl El
impl El
Sourcepub fn primary(self) -> Self
pub fn primary(self) -> Self
Examples found in repository?
97fn settings_header() -> El {
98 row([
99 icon_button("menu").ghost(),
100 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
101 h3("Settings").key("metric:page.title"),
102 spacer(),
103 button("Reset").secondary(),
104 button("Save changes").primary(),
105 ])
106 .key("metric:header")
107 .gap(tokens::SPACE_3)
108 .height(Size::Fixed(56.0))
109 .padding(Sides::xy(tokens::SPACE_4, 0.0))
110 .align(Align::Center)
111 .stroke(tokens::BORDER)
112}More examples
119fn toolbar() -> El {
120 row([
121 column([
122 h1("Polish calibration").key("metric:page.title"),
123 text("A representative app surface for default tuning.")
124 .muted()
125 .key("metric:page.subtitle"),
126 ])
127 .gap(tokens::SPACE_2)
128 .height(Size::Hug),
129 spacer(),
130 button_with_icon("search", "Preview")
131 .secondary()
132 .key("metric:action.secondary"),
133 button_with_icon("upload", "Publish")
134 .primary()
135 .key("metric:action.primary"),
136 ])
137 .key("metric:header")
138 .gap(tokens::SPACE_4)
139 .height(Size::Hug)
140 .align(Align::Start)
141}
142
143fn kpi_card(label: &'static str, value: &'static str, delta: &'static str, positive: bool) -> El {
144 let delta_badge = if positive {
145 badge(delta).success()
146 } else {
147 badge(delta).destructive()
148 };
149 let delta_badge = if label == "Latency" {
150 delta_badge.key("metric:kpi.badge")
151 } else {
152 delta_badge
153 };
154 let value_text = h2(value).display();
155 let value_text = if label == "Latency" {
156 value_text.key("metric:kpi.value")
157 } else {
158 value_text
159 };
160 card([
161 card_header([card_title(label)]),
162 card_content([
163 row([value_text, spacer(), delta_badge]).align(Align::Center),
164 text(if positive {
165 "Moving in the expected direction"
166 } else {
167 "Needs visual attention"
168 })
169 .muted(),
170 ])
171 .gap(tokens::SPACE_6),
172 ])
173 .key(if label == "Latency" {
174 "metric:kpi.card"
175 } else {
176 label
177 })
178 .width(Size::Fill(1.0))
179}
180
181fn table_card() -> El {
182 card([
183 card_header([card_title("Reference rows")]),
184 card_content([table([
185 table_header([table_row([
186 table_head("Status").width(Size::Fixed(86.0)),
187 table_head("Surface").width(Size::Fill(1.0)),
188 table_head("Owner").width(Size::Fixed(110.0)),
189 table_head("State").width(Size::Fixed(86.0)),
190 ])
191 .key("metric:table.header")]),
192 divider(),
193 table_body([
194 data_row("OK", "Settings card", "core", "selected", true, "success"),
195 data_row(
196 "WARN",
197 "Command palette density",
198 "widgets",
199 "needs work",
200 false,
201 "warning",
202 ),
203 data_row(
204 "ERR",
205 "Disabled and invalid states",
206 "style",
207 "missing",
208 false,
209 "destructive",
210 ),
211 data_row(
212 "INFO",
213 "Token resolution",
214 "theme",
215 "planned",
216 false,
217 "info",
218 ),
219 data_row(
220 "OK",
221 "Popover elevation",
222 "shader",
223 "queued",
224 false,
225 "success",
226 ),
227 ])
228 .gap(tokens::SPACE_1)
229 .width(Size::Fill(1.0)),
230 ])]),
231 ])
232 .key("metric:table.card")
233 .width(Size::Fill(1.2))
234 .height(Size::Fill(1.0))
235}
236
237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}
287
288fn command_card() -> El {
289 card([
290 card_header([card_title("Command surface")]),
291 card_content([
292 text_input(
293 "Search commands...",
294 &Selection::default(),
295 "command-search",
296 )
297 .key("metric:command.input")
298 .width(Size::Fill(1.0)),
299 popover_panel([
300 command_row("git-branch", "New branch", "Ctrl+B").key("metric:command.row"),
301 command_row("git-commit", "Commit staged files", "Ctrl+Enter")
302 .key("command-row-commit"),
303 command_row("refresh-cw", "Refresh repository", "Ctrl+R")
304 .key("command-row-refresh"),
305 command_row("alert-circle", "Force push", "Danger").key("command-row-force"),
306 ])
307 .width(Size::Fill(1.0)),
308 scroll([form_probe()]).key("form-probe-scroll"),
309 ])
310 .height(Size::Fill(1.0)),
311 ])
312 .key("metric:command.card")
313 .width(Size::Fill(0.8))
314 .height(Size::Fill(1.0))
315}
316
317fn form_probe() -> El {
318 form([
319 form_item([
320 form_label("Valid input"),
321 form_control(
322 text_input(
323 "Valid input",
324 &Selection::caret("valid-input", 11),
325 "valid-input",
326 )
327 .key("metric:form.input"),
328 ),
329 form_description("Default field spacing and helper text."),
330 ]),
331 form_item([
332 form_label("Invalid input"),
333 form_control(
334 text_input(
335 "Invalid input",
336 &Selection::caret("invalid-input", 13),
337 "invalid-input",
338 )
339 .invalid(),
340 ),
341 form_message("This field needs attention."),
342 ]),
343 row([
344 button("Disabled").secondary().disabled(),
345 button("Loading").primary().loading(),
346 spacer(),
347 ]),
348 ])
349 .padding(tokens::SPACE_3)
350 .fill(tokens::MUTED)
351 .stroke(tokens::BORDER)
352 .radius(tokens::RADIUS_MD)
353}48fn fixture() -> El {
49 let centre = h2("Compass").center_text();
50 let dirs = [
51 ("North", "n"),
52 ("NE", "ne"),
53 ("East", "e"),
54 ("SE", "se"),
55 ("South", "s"),
56 ("SW", "sw"),
57 ("West", "w"),
58 ("NW", "nw"),
59 ];
60
61 let mut children: Vec<El> = vec![centre];
62 for (label, k) in dirs {
63 children.push(button(label).key(k).primary());
64 }
65
66 column([
67 h1("Custom layout — circular"),
68 paragraph(
69 "Eight buttons positioned on a circle by an author-supplied \
70 LayoutFn. Stock paint, automatic hover/press, and hit-test \
71 all keep working — only the rect distribution changed.",
72 )
73 .muted(),
74 stack(children)
75 .key("compass")
76 .layout(circular)
77 .width(Size::Fill(1.0))
78 .height(Size::Fixed(360.0)),
79 ])
80 .gap(tokens::SPACE_4)
81 .padding(tokens::SPACE_7)
82}314fn component_section() -> El {
315 card([
316 card_header([
317 card_title("Stock widgets"),
318 card_description("The same palette applied to regular component constructors."),
319 ]),
320 card_content([
321 row([
322 button("Primary").primary(),
323 button("Secondary").secondary(),
324 button("Outline").outline(),
325 button("Ghost").ghost(),
326 ])
327 .gap(tokens::SPACE_2)
328 .align(Align::Center),
329 row([
330 badge("success").success(),
331 badge("warning").warning(),
332 badge("destructive").destructive(),
333 badge("info").info(),
334 badge("muted").muted(),
335 ])
336 .gap(tokens::SPACE_2)
337 .align(Align::Center),
338 row([
339 text_input("palette search", &Selection::default(), "palette:search")
340 .width(Size::Fill(1.0)),
341 button_with_icon("settings", "Tune").secondary(),
342 ])
343 .gap(tokens::SPACE_2)
344 .align(Align::Center),
345 row([
346 surface_sample("Card", tokens::CARD),
347 surface_sample("Muted", tokens::MUTED),
348 surface_sample("Popover", tokens::POPOVER),
349 ])
350 .gap(tokens::SPACE_3)
351 .align(Align::Stretch),
352 ])
353 .gap(tokens::SPACE_4),
354 ])
355 .height(Size::Hug)
356}16fn settings() -> El {
17 column([
18 h1("Settings"),
19 titled_card(
20 "Account",
21 [
22 row([text("Email"), spacer(), text("user@example.com").muted()]),
23 row([
24 text("Two-factor authentication"),
25 spacer(),
26 badge("Enabled").success(),
27 ]),
28 row([
29 text("Recovery codes"),
30 spacer(),
31 button("Generate").secondary(),
32 ]),
33 ],
34 ),
35 titled_card(
36 "Appearance",
37 [
38 row([text("Theme"), spacer(), button("Dark").secondary()]),
39 row([text("Compact mode"), spacer(), badge("Off").muted()]),
40 row([text("Font size"), spacer(), text("14")]),
41 ],
42 ),
43 titled_card(
44 "Danger zone",
45 [row([
46 column([
47 text("Delete account").bold(),
48 text("Permanently remove your account and all data.")
49 .muted()
50 .small(),
51 ])
52 .gap(tokens::SPACE_1)
53 .align(Align::Start)
54 .width(Size::Hug),
55 spacer(),
56 button("Delete").destructive(),
57 ])],
58 ),
59 row([spacer(), button("Cancel").ghost(), button("Save").primary()]),
60 ])
61 .gap(tokens::SPACE_4)
62 .padding(tokens::SPACE_7)
63}Sourcepub fn success(self) -> Self
pub fn success(self) -> Self
Examples found in repository?
292fn security_card() -> El {
293 card([
294 card_header([
295 card_title("Security"),
296 card_description("Two-factor authentication is enabled for all privileged users."),
297 ]),
298 card_content([
299 compact_stat("Passkeys", "2 registered", badge("On").success()),
300 compact_stat("Sessions", "3 active", button("Review").secondary()),
301 ]),
302 ])
303 .width(Size::Fill(1.0))
304}More examples
18fn build_row(i: usize) -> El {
19 let badge_el = match i % 5 {
20 0 => badge("info").muted(),
21 1 => badge("warn").warning(),
22 2 => badge("ok").success(),
23 3 => badge("err").destructive(),
24 _ => spacer(),
25 };
26 row([
27 text(format!("#{i:05}")).mono(),
28 spacer(),
29 text(format!("entry {i}")),
30 spacer(),
31 badge_el,
32 ])
33 .key(format!("row-{i}"))
34 .gap(tokens::SPACE_3)
35 .padding(Sides::xy(tokens::SPACE_3, tokens::SPACE_2))
36 .height(Size::Fixed(ROW_HEIGHT))
37}143fn kpi_card(label: &'static str, value: &'static str, delta: &'static str, positive: bool) -> El {
144 let delta_badge = if positive {
145 badge(delta).success()
146 } else {
147 badge(delta).destructive()
148 };
149 let delta_badge = if label == "Latency" {
150 delta_badge.key("metric:kpi.badge")
151 } else {
152 delta_badge
153 };
154 let value_text = h2(value).display();
155 let value_text = if label == "Latency" {
156 value_text.key("metric:kpi.value")
157 } else {
158 value_text
159 };
160 card([
161 card_header([card_title(label)]),
162 card_content([
163 row([value_text, spacer(), delta_badge]).align(Align::Center),
164 text(if positive {
165 "Moving in the expected direction"
166 } else {
167 "Needs visual attention"
168 })
169 .muted(),
170 ])
171 .gap(tokens::SPACE_6),
172 ])
173 .key(if label == "Latency" {
174 "metric:kpi.card"
175 } else {
176 label
177 })
178 .width(Size::Fill(1.0))
179}
180
181fn table_card() -> El {
182 card([
183 card_header([card_title("Reference rows")]),
184 card_content([table([
185 table_header([table_row([
186 table_head("Status").width(Size::Fixed(86.0)),
187 table_head("Surface").width(Size::Fill(1.0)),
188 table_head("Owner").width(Size::Fixed(110.0)),
189 table_head("State").width(Size::Fixed(86.0)),
190 ])
191 .key("metric:table.header")]),
192 divider(),
193 table_body([
194 data_row("OK", "Settings card", "core", "selected", true, "success"),
195 data_row(
196 "WARN",
197 "Command palette density",
198 "widgets",
199 "needs work",
200 false,
201 "warning",
202 ),
203 data_row(
204 "ERR",
205 "Disabled and invalid states",
206 "style",
207 "missing",
208 false,
209 "destructive",
210 ),
211 data_row(
212 "INFO",
213 "Token resolution",
214 "theme",
215 "planned",
216 false,
217 "info",
218 ),
219 data_row(
220 "OK",
221 "Popover elevation",
222 "shader",
223 "queued",
224 false,
225 "success",
226 ),
227 ])
228 .gap(tokens::SPACE_1)
229 .width(Size::Fill(1.0)),
230 ])]),
231 ])
232 .key("metric:table.card")
233 .width(Size::Fill(1.2))
234 .height(Size::Fill(1.0))
235}
236
237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}214fn metric_card(
215 icon_name: &'static str,
216 title: &'static str,
217 value: &'static str,
218 delta: &'static str,
219 note: &'static str,
220 positive: bool,
221) -> El {
222 let badge = if positive {
223 badge(delta).success()
224 } else {
225 badge(delta).warning()
226 };
227 let badge = if title == "Total Revenue" {
228 badge.key("metric:kpi.badge")
229 } else {
230 badge
231 };
232 let value = if title == "Total Revenue" {
233 h2(value).ellipsis().key("metric:kpi.value")
234 } else {
235 h2(value).ellipsis()
236 };
237 card([card_content([
238 row([
239 row([
240 icon(icon_name)
241 .color(tokens::MUTED_FOREGROUND)
242 .icon_size(tokens::ICON_XS),
243 text(title).muted().ellipsis().width(Size::Fill(1.0)),
244 ])
245 .gap(tokens::SPACE_1)
246 .width(Size::Fill(1.0))
247 .align(Align::Center),
248 badge,
249 ])
250 .gap(tokens::SPACE_2)
251 .align(Align::Center),
252 value,
253 text(note).caption().ellipsis().width(Size::Fill(1.0)),
254 ])
255 .padding(tokens::SPACE_4)
256 .gap(tokens::SPACE_2)])
257 .key(if title == "Total Revenue" {
258 "metric:kpi.card"
259 } else {
260 title
261 })
262 .width(Size::Fill(1.0))
263}
264
265fn chart_card() -> El {
266 card([
267 card_header([
268 card_title("Visitors for the last 6 months"),
269 card_description("Total visitors by channel."),
270 ])
271 .padding(tokens::SPACE_4),
272 card_content([row(chart_bars())
273 .gap(2.0)
274 .height(Size::Fill(1.0))
275 .align(Align::End)])
276 .padding(Sides {
277 left: tokens::SPACE_4,
278 right: tokens::SPACE_4,
279 top: 0.0,
280 bottom: tokens::SPACE_4,
281 })
282 .height(Size::Fill(1.0)),
283 ])
284 .key("metric:chart.card")
285 .width(Size::Fill(1.0))
286 .height(Size::Fill(1.0))
287}
288
289fn chart_bars() -> Vec<El> {
290 [
291 48.0, 72.0, 56.0, 90.0, 64.0, 80.0, 108.0, 84.0, 122.0, 96.0, 136.0, 118.0,
292 ]
293 .into_iter()
294 .flat_map(|height| {
295 [
296 bar(height, tokens::MUTED_FOREGROUND),
297 bar((height - 28.0_f32).max(24.0), tokens::INPUT),
298 ]
299 })
300 .collect()
301}
302
303fn bar(height: f32, color: Color) -> El {
304 El::new(Kind::Custom("chart_bar"))
305 .fill(color)
306 .radius(tokens::RADIUS_SM)
307 .width(Size::Fill(1.0))
308 .height(Size::Fixed(height))
309}
310
311fn sales_card() -> El {
312 card([
313 card_header([
314 card_title("Recent Sales"),
315 card_description("You made 265 sales this month."),
316 ])
317 .padding(tokens::SPACE_4),
318 card_content([
319 sale_row("OM", "Olivia Martin", "olivia@example.com", "+$1,999.00"),
320 sale_row("JL", "Jackson Lee", "jackson@example.com", "+$39.00"),
321 sale_row("IN", "Isabella Nguyen", "isabella@example.com", "+$299.00"),
322 sale_row("WK", "William Kim", "will@example.com", "+$99.00"),
323 ])
324 .gap(tokens::SPACE_2)
325 .padding(Sides {
326 left: tokens::SPACE_4,
327 right: tokens::SPACE_4,
328 top: 0.0,
329 bottom: tokens::SPACE_4,
330 }),
331 ])
332 .key("metric:sales.card")
333 .width(Size::Fixed(330.0))
334 .height(Size::Fill(1.0))
335}
336
337fn sale_row(
338 initials: &'static str,
339 name: &'static str,
340 email: &'static str,
341 amount: &'static str,
342) -> El {
343 row([
344 icon_cell(initials),
345 column([
346 text(name).semibold().ellipsis().width(Size::Fill(1.0)),
347 text(email).caption().ellipsis().width(Size::Fill(1.0)),
348 ])
349 .gap(2.0)
350 .height(Size::Hug)
351 .width(Size::Fill(1.0)),
352 text(amount).label().small(),
353 ])
354 .gap(tokens::SPACE_2)
355 .height(Size::Fixed(42.0))
356 .align(Align::Center)
357}
358
359fn documents_card() -> El {
360 card([
361 card_header([card_title("Documents")]).padding(tokens::SPACE_4),
362 card_content([scroll([table([
363 table_header([table_row([
364 table_head("").width(Size::Fixed(35.0)),
365 table_head("Header").width(Size::Fill(1.8)),
366 table_head("Section Type").width(Size::Fill(1.0)),
367 table_head("Status").width(Size::Fixed(104.0)),
368 table_head("Target").width(Size::Fixed(64.0)),
369 table_head("Limit").width(Size::Fixed(64.0)),
370 table_head("Reviewer").width(Size::Fixed(128.0)),
371 table_head("").width(Size::Fixed(32.0)),
372 ])
373 .padding(Sides::xy(tokens::SPACE_4, 0.0))
374 .key("metric:table.header")]),
375 divider(),
376 table_body([
377 document_row(
378 "Cover page",
379 "Cover page",
380 "In Process",
381 "18",
382 "5",
383 "Eddie Lake",
384 "info",
385 ),
386 document_row(
387 "Table of contents",
388 "Table of contents",
389 "Done",
390 "29",
391 "24",
392 "Eddie Lake",
393 "success",
394 ),
395 ]),
396 ])])
397 .height(Size::Fill(1.0))])
398 .gap(0.0)
399 .padding(0.0)
400 .height(Size::Fill(1.0)),
401 ])
402 .key("metric:table.card")
403 .height(Size::Fill(1.0))
404}
405
406fn document_row(
407 header: &'static str,
408 section: &'static str,
409 status: &'static str,
410 target: &'static str,
411 limit: &'static str,
412 reviewer: &'static str,
413 tone: &'static str,
414) -> El {
415 let status_badge = match tone {
416 "success" => badge(status).success(),
417 _ => badge(status).info(),
418 };
419 table_row([
420 table_utility_cell("::"),
421 table_cell(text(header).label().small()).width(Size::Fill(1.8)),
422 table_cell(text(section).muted()).width(Size::Fill(1.0)),
423 table_cell(status_badge).width(Size::Fixed(104.0)),
424 table_cell(text(target).label().small()).width(Size::Fixed(64.0)),
425 table_cell(text(limit).label().small()).width(Size::Fixed(64.0)),
426 table_cell(text(reviewer).muted()).width(Size::Fixed(128.0)),
427 table_action_cell(),
428 ])
429 .padding(Sides::xy(tokens::SPACE_4, 0.0))
430 .key(if header == "Cover page" {
431 "metric:table.row"
432 } else {
433 header
434 })
435}314fn component_section() -> El {
315 card([
316 card_header([
317 card_title("Stock widgets"),
318 card_description("The same palette applied to regular component constructors."),
319 ]),
320 card_content([
321 row([
322 button("Primary").primary(),
323 button("Secondary").secondary(),
324 button("Outline").outline(),
325 button("Ghost").ghost(),
326 ])
327 .gap(tokens::SPACE_2)
328 .align(Align::Center),
329 row([
330 badge("success").success(),
331 badge("warning").warning(),
332 badge("destructive").destructive(),
333 badge("info").info(),
334 badge("muted").muted(),
335 ])
336 .gap(tokens::SPACE_2)
337 .align(Align::Center),
338 row([
339 text_input("palette search", &Selection::default(), "palette:search")
340 .width(Size::Fill(1.0)),
341 button_with_icon("settings", "Tune").secondary(),
342 ])
343 .gap(tokens::SPACE_2)
344 .align(Align::Center),
345 row([
346 surface_sample("Card", tokens::CARD),
347 surface_sample("Muted", tokens::MUTED),
348 surface_sample("Popover", tokens::POPOVER),
349 ])
350 .gap(tokens::SPACE_3)
351 .align(Align::Stretch),
352 ])
353 .gap(tokens::SPACE_4),
354 ])
355 .height(Size::Hug)
356}16fn settings() -> El {
17 column([
18 h1("Settings"),
19 titled_card(
20 "Account",
21 [
22 row([text("Email"), spacer(), text("user@example.com").muted()]),
23 row([
24 text("Two-factor authentication"),
25 spacer(),
26 badge("Enabled").success(),
27 ]),
28 row([
29 text("Recovery codes"),
30 spacer(),
31 button("Generate").secondary(),
32 ]),
33 ],
34 ),
35 titled_card(
36 "Appearance",
37 [
38 row([text("Theme"), spacer(), button("Dark").secondary()]),
39 row([text("Compact mode"), spacer(), badge("Off").muted()]),
40 row([text("Font size"), spacer(), text("14")]),
41 ],
42 ),
43 titled_card(
44 "Danger zone",
45 [row([
46 column([
47 text("Delete account").bold(),
48 text("Permanently remove your account and all data.")
49 .muted()
50 .small(),
51 ])
52 .gap(tokens::SPACE_1)
53 .align(Align::Start)
54 .width(Size::Hug),
55 spacer(),
56 button("Delete").destructive(),
57 ])],
58 ),
59 row([spacer(), button("Cancel").ghost(), button("Save").primary()]),
60 ])
61 .gap(tokens::SPACE_4)
62 .padding(tokens::SPACE_7)
63}Sourcepub fn warning(self) -> Self
pub fn warning(self) -> Self
Examples found in repository?
18fn build_row(i: usize) -> El {
19 let badge_el = match i % 5 {
20 0 => badge("info").muted(),
21 1 => badge("warn").warning(),
22 2 => badge("ok").success(),
23 3 => badge("err").destructive(),
24 _ => spacer(),
25 };
26 row([
27 text(format!("#{i:05}")).mono(),
28 spacer(),
29 text(format!("entry {i}")),
30 spacer(),
31 badge_el,
32 ])
33 .key(format!("row-{i}"))
34 .gap(tokens::SPACE_3)
35 .padding(Sides::xy(tokens::SPACE_3, tokens::SPACE_2))
36 .height(Size::Fixed(ROW_HEIGHT))
37}More examples
214fn metric_card(
215 icon_name: &'static str,
216 title: &'static str,
217 value: &'static str,
218 delta: &'static str,
219 note: &'static str,
220 positive: bool,
221) -> El {
222 let badge = if positive {
223 badge(delta).success()
224 } else {
225 badge(delta).warning()
226 };
227 let badge = if title == "Total Revenue" {
228 badge.key("metric:kpi.badge")
229 } else {
230 badge
231 };
232 let value = if title == "Total Revenue" {
233 h2(value).ellipsis().key("metric:kpi.value")
234 } else {
235 h2(value).ellipsis()
236 };
237 card([card_content([
238 row([
239 row([
240 icon(icon_name)
241 .color(tokens::MUTED_FOREGROUND)
242 .icon_size(tokens::ICON_XS),
243 text(title).muted().ellipsis().width(Size::Fill(1.0)),
244 ])
245 .gap(tokens::SPACE_1)
246 .width(Size::Fill(1.0))
247 .align(Align::Center),
248 badge,
249 ])
250 .gap(tokens::SPACE_2)
251 .align(Align::Center),
252 value,
253 text(note).caption().ellipsis().width(Size::Fill(1.0)),
254 ])
255 .padding(tokens::SPACE_4)
256 .gap(tokens::SPACE_2)])
257 .key(if title == "Total Revenue" {
258 "metric:kpi.card"
259 } else {
260 title
261 })
262 .width(Size::Fill(1.0))
263}237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}314fn component_section() -> El {
315 card([
316 card_header([
317 card_title("Stock widgets"),
318 card_description("The same palette applied to regular component constructors."),
319 ]),
320 card_content([
321 row([
322 button("Primary").primary(),
323 button("Secondary").secondary(),
324 button("Outline").outline(),
325 button("Ghost").ghost(),
326 ])
327 .gap(tokens::SPACE_2)
328 .align(Align::Center),
329 row([
330 badge("success").success(),
331 badge("warning").warning(),
332 badge("destructive").destructive(),
333 badge("info").info(),
334 badge("muted").muted(),
335 ])
336 .gap(tokens::SPACE_2)
337 .align(Align::Center),
338 row([
339 text_input("palette search", &Selection::default(), "palette:search")
340 .width(Size::Fill(1.0)),
341 button_with_icon("settings", "Tune").secondary(),
342 ])
343 .gap(tokens::SPACE_2)
344 .align(Align::Center),
345 row([
346 surface_sample("Card", tokens::CARD),
347 surface_sample("Muted", tokens::MUTED),
348 surface_sample("Popover", tokens::POPOVER),
349 ])
350 .gap(tokens::SPACE_3)
351 .align(Align::Stretch),
352 ])
353 .gap(tokens::SPACE_4),
354 ])
355 .height(Size::Hug)
356}Sourcepub fn destructive(self) -> Self
pub fn destructive(self) -> Self
Examples found in repository?
18fn build_row(i: usize) -> El {
19 let badge_el = match i % 5 {
20 0 => badge("info").muted(),
21 1 => badge("warn").warning(),
22 2 => badge("ok").success(),
23 3 => badge("err").destructive(),
24 _ => spacer(),
25 };
26 row([
27 text(format!("#{i:05}")).mono(),
28 spacer(),
29 text(format!("entry {i}")),
30 spacer(),
31 badge_el,
32 ])
33 .key(format!("row-{i}"))
34 .gap(tokens::SPACE_3)
35 .padding(Sides::xy(tokens::SPACE_3, tokens::SPACE_2))
36 .height(Size::Fixed(ROW_HEIGHT))
37}More examples
143fn kpi_card(label: &'static str, value: &'static str, delta: &'static str, positive: bool) -> El {
144 let delta_badge = if positive {
145 badge(delta).success()
146 } else {
147 badge(delta).destructive()
148 };
149 let delta_badge = if label == "Latency" {
150 delta_badge.key("metric:kpi.badge")
151 } else {
152 delta_badge
153 };
154 let value_text = h2(value).display();
155 let value_text = if label == "Latency" {
156 value_text.key("metric:kpi.value")
157 } else {
158 value_text
159 };
160 card([
161 card_header([card_title(label)]),
162 card_content([
163 row([value_text, spacer(), delta_badge]).align(Align::Center),
164 text(if positive {
165 "Moving in the expected direction"
166 } else {
167 "Needs visual attention"
168 })
169 .muted(),
170 ])
171 .gap(tokens::SPACE_6),
172 ])
173 .key(if label == "Latency" {
174 "metric:kpi.card"
175 } else {
176 label
177 })
178 .width(Size::Fill(1.0))
179}
180
181fn table_card() -> El {
182 card([
183 card_header([card_title("Reference rows")]),
184 card_content([table([
185 table_header([table_row([
186 table_head("Status").width(Size::Fixed(86.0)),
187 table_head("Surface").width(Size::Fill(1.0)),
188 table_head("Owner").width(Size::Fixed(110.0)),
189 table_head("State").width(Size::Fixed(86.0)),
190 ])
191 .key("metric:table.header")]),
192 divider(),
193 table_body([
194 data_row("OK", "Settings card", "core", "selected", true, "success"),
195 data_row(
196 "WARN",
197 "Command palette density",
198 "widgets",
199 "needs work",
200 false,
201 "warning",
202 ),
203 data_row(
204 "ERR",
205 "Disabled and invalid states",
206 "style",
207 "missing",
208 false,
209 "destructive",
210 ),
211 data_row(
212 "INFO",
213 "Token resolution",
214 "theme",
215 "planned",
216 false,
217 "info",
218 ),
219 data_row(
220 "OK",
221 "Popover elevation",
222 "shader",
223 "queued",
224 false,
225 "success",
226 ),
227 ])
228 .gap(tokens::SPACE_1)
229 .width(Size::Fill(1.0)),
230 ])]),
231 ])
232 .key("metric:table.card")
233 .width(Size::Fill(1.2))
234 .height(Size::Fill(1.0))
235}
236
237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}9fn modal_fixture() -> El {
10 stack([
11 column([
12 h1("Account"),
13 titled_card(
14 "Profile",
15 [
16 row([text("Email"), spacer(), text("user@example.com").muted()]),
17 row([text("Plan"), spacer(), badge("Pro").info()]),
18 ],
19 ),
20 titled_card(
21 "Danger zone",
22 [row([
23 column([
24 text("Delete account").bold(),
25 text("Remove this account and all associated data.")
26 .muted()
27 .small(),
28 ])
29 .gap(tokens::SPACE_1)
30 .align(Align::Start)
31 .width(Size::Hug),
32 spacer(),
33 button("Delete").destructive().key("open-delete"),
34 ])],
35 ),
36 ])
37 .gap(tokens::SPACE_4)
38 .padding(tokens::SPACE_7),
39 modal(
40 "delete-account",
41 "Delete account?",
42 [
43 text("Permanent action. Export data first.").muted(),
44 row([
45 spacer(),
46 button("Cancel").ghost().key("cancel-delete"),
47 button("Delete").destructive().key("confirm-delete"),
48 ])
49 .gap(tokens::SPACE_2),
50 ],
51 ),
52 ])
53}314fn component_section() -> El {
315 card([
316 card_header([
317 card_title("Stock widgets"),
318 card_description("The same palette applied to regular component constructors."),
319 ]),
320 card_content([
321 row([
322 button("Primary").primary(),
323 button("Secondary").secondary(),
324 button("Outline").outline(),
325 button("Ghost").ghost(),
326 ])
327 .gap(tokens::SPACE_2)
328 .align(Align::Center),
329 row([
330 badge("success").success(),
331 badge("warning").warning(),
332 badge("destructive").destructive(),
333 badge("info").info(),
334 badge("muted").muted(),
335 ])
336 .gap(tokens::SPACE_2)
337 .align(Align::Center),
338 row([
339 text_input("palette search", &Selection::default(), "palette:search")
340 .width(Size::Fill(1.0)),
341 button_with_icon("settings", "Tune").secondary(),
342 ])
343 .gap(tokens::SPACE_2)
344 .align(Align::Center),
345 row([
346 surface_sample("Card", tokens::CARD),
347 surface_sample("Muted", tokens::MUTED),
348 surface_sample("Popover", tokens::POPOVER),
349 ])
350 .gap(tokens::SPACE_3)
351 .align(Align::Stretch),
352 ])
353 .gap(tokens::SPACE_4),
354 ])
355 .height(Size::Hug)
356}16fn settings() -> El {
17 column([
18 h1("Settings"),
19 titled_card(
20 "Account",
21 [
22 row([text("Email"), spacer(), text("user@example.com").muted()]),
23 row([
24 text("Two-factor authentication"),
25 spacer(),
26 badge("Enabled").success(),
27 ]),
28 row([
29 text("Recovery codes"),
30 spacer(),
31 button("Generate").secondary(),
32 ]),
33 ],
34 ),
35 titled_card(
36 "Appearance",
37 [
38 row([text("Theme"), spacer(), button("Dark").secondary()]),
39 row([text("Compact mode"), spacer(), badge("Off").muted()]),
40 row([text("Font size"), spacer(), text("14")]),
41 ],
42 ),
43 titled_card(
44 "Danger zone",
45 [row([
46 column([
47 text("Delete account").bold(),
48 text("Permanently remove your account and all data.")
49 .muted()
50 .small(),
51 ])
52 .gap(tokens::SPACE_1)
53 .align(Align::Start)
54 .width(Size::Hug),
55 spacer(),
56 button("Delete").destructive(),
57 ])],
58 ),
59 row([spacer(), button("Cancel").ghost(), button("Save").primary()]),
60 ])
61 .gap(tokens::SPACE_4)
62 .padding(tokens::SPACE_7)
63}Sourcepub fn info(self) -> Self
pub fn info(self) -> Self
Examples found in repository?
15fn scroll_list_fixture() -> El {
16 let rows: Vec<El> = (0..20)
17 .map(|i| {
18 row([
19 badge(format!("#{i}")).info(),
20 text(format!("Notification {i}")).bold(),
21 spacer(),
22 text(format!("{}m ago", i + 1)).muted(),
23 ])
24 .gap(tokens::SPACE_2)
25 .height(Size::Fixed(44.0))
26 .padding(Sides::xy(tokens::SPACE_3, tokens::SPACE_2))
27 })
28 .collect();
29
30 let list = scroll(rows)
31 .key("notifications")
32 .height(Size::Fixed(420.0))
33 .padding(tokens::SPACE_2);
34
35 column([
36 h2("Notifications"),
37 text("Roll the wheel inside the panel to scroll. The content is taller than the viewport.")
38 .muted(),
39 list,
40 ])
41 .gap(tokens::SPACE_4)
42 .padding(tokens::SPACE_7)
43}More examples
406fn document_row(
407 header: &'static str,
408 section: &'static str,
409 status: &'static str,
410 target: &'static str,
411 limit: &'static str,
412 reviewer: &'static str,
413 tone: &'static str,
414) -> El {
415 let status_badge = match tone {
416 "success" => badge(status).success(),
417 _ => badge(status).info(),
418 };
419 table_row([
420 table_utility_cell("::"),
421 table_cell(text(header).label().small()).width(Size::Fill(1.8)),
422 table_cell(text(section).muted()).width(Size::Fill(1.0)),
423 table_cell(status_badge).width(Size::Fixed(104.0)),
424 table_cell(text(target).label().small()).width(Size::Fixed(64.0)),
425 table_cell(text(limit).label().small()).width(Size::Fixed(64.0)),
426 table_cell(text(reviewer).muted()).width(Size::Fixed(128.0)),
427 table_action_cell(),
428 ])
429 .padding(Sides::xy(tokens::SPACE_4, 0.0))
430 .key(if header == "Cover page" {
431 "metric:table.row"
432 } else {
433 header
434 })
435}237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}9fn modal_fixture() -> El {
10 stack([
11 column([
12 h1("Account"),
13 titled_card(
14 "Profile",
15 [
16 row([text("Email"), spacer(), text("user@example.com").muted()]),
17 row([text("Plan"), spacer(), badge("Pro").info()]),
18 ],
19 ),
20 titled_card(
21 "Danger zone",
22 [row([
23 column([
24 text("Delete account").bold(),
25 text("Remove this account and all associated data.")
26 .muted()
27 .small(),
28 ])
29 .gap(tokens::SPACE_1)
30 .align(Align::Start)
31 .width(Size::Hug),
32 spacer(),
33 button("Delete").destructive().key("open-delete"),
34 ])],
35 ),
36 ])
37 .gap(tokens::SPACE_4)
38 .padding(tokens::SPACE_7),
39 modal(
40 "delete-account",
41 "Delete account?",
42 [
43 text("Permanent action. Export data first.").muted(),
44 row([
45 spacer(),
46 button("Cancel").ghost().key("cancel-delete"),
47 button("Delete").destructive().key("confirm-delete"),
48 ])
49 .gap(tokens::SPACE_2),
50 ],
51 ),
52 ])
53}314fn component_section() -> El {
315 card([
316 card_header([
317 card_title("Stock widgets"),
318 card_description("The same palette applied to regular component constructors."),
319 ]),
320 card_content([
321 row([
322 button("Primary").primary(),
323 button("Secondary").secondary(),
324 button("Outline").outline(),
325 button("Ghost").ghost(),
326 ])
327 .gap(tokens::SPACE_2)
328 .align(Align::Center),
329 row([
330 badge("success").success(),
331 badge("warning").warning(),
332 badge("destructive").destructive(),
333 badge("info").info(),
334 badge("muted").muted(),
335 ])
336 .gap(tokens::SPACE_2)
337 .align(Align::Center),
338 row([
339 text_input("palette search", &Selection::default(), "palette:search")
340 .width(Size::Fill(1.0)),
341 button_with_icon("settings", "Tune").secondary(),
342 ])
343 .gap(tokens::SPACE_2)
344 .align(Align::Center),
345 row([
346 surface_sample("Card", tokens::CARD),
347 surface_sample("Muted", tokens::MUTED),
348 surface_sample("Popover", tokens::POPOVER),
349 ])
350 .gap(tokens::SPACE_3)
351 .align(Align::Stretch),
352 ])
353 .gap(tokens::SPACE_4),
354 ])
355 .height(Size::Hug)
356}Sourcepub fn secondary(self) -> Self
pub fn secondary(self) -> Self
Default-styled secondary surface. This is the default look for
button(...); calling .secondary() makes intent explicit.
Examples found in repository?
97fn settings_header() -> El {
98 row([
99 icon_button("menu").ghost(),
100 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
101 h3("Settings").key("metric:page.title"),
102 spacer(),
103 button("Reset").secondary(),
104 button("Save changes").primary(),
105 ])
106 .key("metric:header")
107 .gap(tokens::SPACE_3)
108 .height(Size::Fixed(56.0))
109 .padding(Sides::xy(tokens::SPACE_4, 0.0))
110 .align(Align::Center)
111 .stroke(tokens::BORDER)
112}
113
114fn settings_nav_card() -> El {
115 column([
116 settings_nav_item("Account", true),
117 settings_nav_item("Security", false),
118 settings_nav_item("Notifications", false),
119 settings_nav_item("Appearance", false),
120 settings_nav_item("Billing", false),
121 ])
122 .gap(tokens::SPACE_1)
123 .padding(tokens::SPACE_1)
124 .width(Size::Fixed(220.0))
125 .height(Size::Fill(1.0))
126 .style_profile(StyleProfile::Surface)
127 .surface_role(SurfaceRole::Panel)
128 .fill(tokens::CARD)
129 .stroke(tokens::BORDER)
130 .radius(tokens::RADIUS_MD)
131 .shadow(tokens::SHADOW_MD)
132}
133
134fn settings_nav_item(label: &'static str, selected: bool) -> El {
135 let mut item = row([
136 El::new(Kind::Custom("nav-dot"))
137 .fill(tokens::MUTED_FOREGROUND)
138 .radius(tokens::RADIUS_PILL)
139 .width(Size::Fixed(6.0))
140 .height(Size::Fixed(6.0)),
141 text(label)
142 .font_weight(FontWeight::Medium)
143 .ellipsis()
144 .width(Size::Fill(1.0)),
145 ])
146 .key(if selected {
147 "metric:settings.nav.row".to_string()
148 } else {
149 format!("settings-nav-{label}")
150 })
151 .metrics_role(MetricsRole::ListItem)
152 .align(Align::Center)
153 .focusable();
154
155 if selected {
156 item = item.current();
157 } else {
158 item = item.color(tokens::MUTED_FOREGROUND);
159 }
160
161 item
162}
163
164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}
185
186fn profile_card() -> El {
187 card([
188 card_header([
189 card_title("Profile"),
190 card_description("This information appears in audit logs and shared documents."),
191 ]),
192 card_content([form([
193 row([
194 setting_field("Display name", "Alicia Koch", "display-name"),
195 setting_field("Email", "alicia@acme.co", "email"),
196 ])
197 .gap(tokens::SPACE_3),
198 row([
199 setting_select("Role", "Workspace admin", "role"),
200 setting_select("Region", "US East", "region"),
201 ])
202 .gap(tokens::SPACE_3),
203 ])]),
204 ])
205 .key("metric:profile.card")
206}
207
208fn setting_field(label: &'static str, value: &'static str, key: &'static str) -> El {
209 form_item([
210 form_label(label),
211 form_control(
212 text_input(value, &Selection::caret(key, value.len()), key).key(
213 if key == "display-name" {
214 "metric:form.input"
215 } else {
216 key
217 },
218 ),
219 ),
220 ])
221 .width(Size::Fill(1.0))
222}
223
224fn setting_select(label: &'static str, value: &'static str, key: &'static str) -> El {
225 form_item([form_label(label), form_control(select_trigger(key, value))]).width(Size::Fill(1.0))
226}
227
228fn preferences_card() -> El {
229 card([
230 card_header([
231 card_title("Preferences"),
232 card_description("Defaults used when creating new dashboards and exports."),
233 ]),
234 card_content([column([
235 preference_row(
236 "Compact navigation",
237 "Use tighter rows in the sidebar and command menus.",
238 switch(true).key("compact-navigation"),
239 ),
240 divider(),
241 preference_row(
242 "Email summaries",
243 "Send a daily digest when documents change.",
244 switch(false).key("email-summaries"),
245 ),
246 divider(),
247 preference_row(
248 "Require approval",
249 "Route external sharing through an owner review.",
250 checkbox(true).key("approval-required"),
251 ),
252 ])
253 .gap(0.0)
254 .width(Size::Fill(1.0))])
255 .padding(0.0),
256 ])
257 .key("metric:preferences.card")
258}
259
260fn preference_row(title: &'static str, description: &'static str, control: El) -> El {
261 row([
262 column([
263 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
264 text(description)
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0))
271 .height(Size::Hug),
272 control,
273 ])
274 .key(if title == "Compact navigation" {
275 "metric:preference.row".to_string()
276 } else {
277 format!("preference-{title}")
278 })
279 .metrics_role(MetricsRole::PreferenceRow)
280 .gap(tokens::SPACE_4)
281 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_3))
282 .align(Align::Center)
283}
284
285fn settings_aside() -> El {
286 column([security_card(), scale_card()])
287 .gap(tokens::SPACE_4)
288 .width(Size::Fixed(300.0))
289 .height(Size::Fill(1.0))
290}
291
292fn security_card() -> El {
293 card([
294 card_header([
295 card_title("Security"),
296 card_description("Two-factor authentication is enabled for all privileged users."),
297 ]),
298 card_content([
299 compact_stat("Passkeys", "2 registered", badge("On").success()),
300 compact_stat("Sessions", "3 active", button("Review").secondary()),
301 ]),
302 ])
303 .width(Size::Fill(1.0))
304}More examples
119fn toolbar() -> El {
120 row([
121 column([
122 h1("Polish calibration").key("metric:page.title"),
123 text("A representative app surface for default tuning.")
124 .muted()
125 .key("metric:page.subtitle"),
126 ])
127 .gap(tokens::SPACE_2)
128 .height(Size::Hug),
129 spacer(),
130 button_with_icon("search", "Preview")
131 .secondary()
132 .key("metric:action.secondary"),
133 button_with_icon("upload", "Publish")
134 .primary()
135 .key("metric:action.primary"),
136 ])
137 .key("metric:header")
138 .gap(tokens::SPACE_4)
139 .height(Size::Hug)
140 .align(Align::Start)
141}
142
143fn kpi_card(label: &'static str, value: &'static str, delta: &'static str, positive: bool) -> El {
144 let delta_badge = if positive {
145 badge(delta).success()
146 } else {
147 badge(delta).destructive()
148 };
149 let delta_badge = if label == "Latency" {
150 delta_badge.key("metric:kpi.badge")
151 } else {
152 delta_badge
153 };
154 let value_text = h2(value).display();
155 let value_text = if label == "Latency" {
156 value_text.key("metric:kpi.value")
157 } else {
158 value_text
159 };
160 card([
161 card_header([card_title(label)]),
162 card_content([
163 row([value_text, spacer(), delta_badge]).align(Align::Center),
164 text(if positive {
165 "Moving in the expected direction"
166 } else {
167 "Needs visual attention"
168 })
169 .muted(),
170 ])
171 .gap(tokens::SPACE_6),
172 ])
173 .key(if label == "Latency" {
174 "metric:kpi.card"
175 } else {
176 label
177 })
178 .width(Size::Fill(1.0))
179}
180
181fn table_card() -> El {
182 card([
183 card_header([card_title("Reference rows")]),
184 card_content([table([
185 table_header([table_row([
186 table_head("Status").width(Size::Fixed(86.0)),
187 table_head("Surface").width(Size::Fill(1.0)),
188 table_head("Owner").width(Size::Fixed(110.0)),
189 table_head("State").width(Size::Fixed(86.0)),
190 ])
191 .key("metric:table.header")]),
192 divider(),
193 table_body([
194 data_row("OK", "Settings card", "core", "selected", true, "success"),
195 data_row(
196 "WARN",
197 "Command palette density",
198 "widgets",
199 "needs work",
200 false,
201 "warning",
202 ),
203 data_row(
204 "ERR",
205 "Disabled and invalid states",
206 "style",
207 "missing",
208 false,
209 "destructive",
210 ),
211 data_row(
212 "INFO",
213 "Token resolution",
214 "theme",
215 "planned",
216 false,
217 "info",
218 ),
219 data_row(
220 "OK",
221 "Popover elevation",
222 "shader",
223 "queued",
224 false,
225 "success",
226 ),
227 ])
228 .gap(tokens::SPACE_1)
229 .width(Size::Fill(1.0)),
230 ])]),
231 ])
232 .key("metric:table.card")
233 .width(Size::Fill(1.2))
234 .height(Size::Fill(1.0))
235}
236
237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}
287
288fn command_card() -> El {
289 card([
290 card_header([card_title("Command surface")]),
291 card_content([
292 text_input(
293 "Search commands...",
294 &Selection::default(),
295 "command-search",
296 )
297 .key("metric:command.input")
298 .width(Size::Fill(1.0)),
299 popover_panel([
300 command_row("git-branch", "New branch", "Ctrl+B").key("metric:command.row"),
301 command_row("git-commit", "Commit staged files", "Ctrl+Enter")
302 .key("command-row-commit"),
303 command_row("refresh-cw", "Refresh repository", "Ctrl+R")
304 .key("command-row-refresh"),
305 command_row("alert-circle", "Force push", "Danger").key("command-row-force"),
306 ])
307 .width(Size::Fill(1.0)),
308 scroll([form_probe()]).key("form-probe-scroll"),
309 ])
310 .height(Size::Fill(1.0)),
311 ])
312 .key("metric:command.card")
313 .width(Size::Fill(0.8))
314 .height(Size::Fill(1.0))
315}
316
317fn form_probe() -> El {
318 form([
319 form_item([
320 form_label("Valid input"),
321 form_control(
322 text_input(
323 "Valid input",
324 &Selection::caret("valid-input", 11),
325 "valid-input",
326 )
327 .key("metric:form.input"),
328 ),
329 form_description("Default field spacing and helper text."),
330 ]),
331 form_item([
332 form_label("Invalid input"),
333 form_control(
334 text_input(
335 "Invalid input",
336 &Selection::caret("invalid-input", 13),
337 "invalid-input",
338 )
339 .invalid(),
340 ),
341 form_message("This field needs attention."),
342 ]),
343 row([
344 button("Disabled").secondary().disabled(),
345 button("Loading").primary().loading(),
346 spacer(),
347 ]),
348 ])
349 .padding(tokens::SPACE_3)
350 .fill(tokens::MUTED)
351 .stroke(tokens::BORDER)
352 .radius(tokens::RADIUS_MD)
353}31fn fixture() -> El {
32 column([
33 h1("Custom shader demo"),
34 paragraph(
35 "Three buttons below paint via a registered custom shader \
36 (gradient.wgsl). The right-hand button is a stock rounded_rect \
37 for contrast.",
38 )
39 .muted(),
40 titled_card(
41 "gradient.wgsl — vertical linear gradient",
42 [row([
43 gradient_button(
44 "Sunrise",
45 Color::rgb(255, 200, 90),
46 Color::rgb(245, 95, 110),
47 tokens::RADIUS_MD,
48 ),
49 gradient_button(
50 "Ocean",
51 Color::rgb(120, 200, 255),
52 Color::rgb(40, 90, 200),
53 tokens::RADIUS_MD,
54 ),
55 gradient_button(
56 "Forest",
57 Color::rgb(180, 230, 140),
58 Color::rgb(40, 110, 80),
59 tokens::RADIUS_MD,
60 ),
61 spacer(),
62 button("Stock").secondary(),
63 ])
64 .gap(tokens::SPACE_3)],
65 ),
66 ])
67 .gap(tokens::SPACE_4)
68 .padding(tokens::SPACE_7)
69}314fn component_section() -> El {
315 card([
316 card_header([
317 card_title("Stock widgets"),
318 card_description("The same palette applied to regular component constructors."),
319 ]),
320 card_content([
321 row([
322 button("Primary").primary(),
323 button("Secondary").secondary(),
324 button("Outline").outline(),
325 button("Ghost").ghost(),
326 ])
327 .gap(tokens::SPACE_2)
328 .align(Align::Center),
329 row([
330 badge("success").success(),
331 badge("warning").warning(),
332 badge("destructive").destructive(),
333 badge("info").info(),
334 badge("muted").muted(),
335 ])
336 .gap(tokens::SPACE_2)
337 .align(Align::Center),
338 row([
339 text_input("palette search", &Selection::default(), "palette:search")
340 .width(Size::Fill(1.0)),
341 button_with_icon("settings", "Tune").secondary(),
342 ])
343 .gap(tokens::SPACE_2)
344 .align(Align::Center),
345 row([
346 surface_sample("Card", tokens::CARD),
347 surface_sample("Muted", tokens::MUTED),
348 surface_sample("Popover", tokens::POPOVER),
349 ])
350 .gap(tokens::SPACE_3)
351 .align(Align::Stretch),
352 ])
353 .gap(tokens::SPACE_4),
354 ])
355 .height(Size::Hug)
356}16fn settings() -> El {
17 column([
18 h1("Settings"),
19 titled_card(
20 "Account",
21 [
22 row([text("Email"), spacer(), text("user@example.com").muted()]),
23 row([
24 text("Two-factor authentication"),
25 spacer(),
26 badge("Enabled").success(),
27 ]),
28 row([
29 text("Recovery codes"),
30 spacer(),
31 button("Generate").secondary(),
32 ]),
33 ],
34 ),
35 titled_card(
36 "Appearance",
37 [
38 row([text("Theme"), spacer(), button("Dark").secondary()]),
39 row([text("Compact mode"), spacer(), badge("Off").muted()]),
40 row([text("Font size"), spacer(), text("14")]),
41 ],
42 ),
43 titled_card(
44 "Danger zone",
45 [row([
46 column([
47 text("Delete account").bold(),
48 text("Permanently remove your account and all data.")
49 .muted()
50 .small(),
51 ])
52 .gap(tokens::SPACE_1)
53 .align(Align::Start)
54 .width(Size::Hug),
55 spacer(),
56 button("Delete").destructive(),
57 ])],
58 ),
59 row([spacer(), button("Cancel").ghost(), button("Save").primary()]),
60 ])
61 .gap(tokens::SPACE_4)
62 .padding(tokens::SPACE_7)
63}Sourcepub fn ghost(self) -> Self
pub fn ghost(self) -> Self
No fill, no border. Low-emphasis actions like “Cancel” alongside a primary “Save”.
Examples found in repository?
97fn settings_header() -> El {
98 row([
99 icon_button("menu").ghost(),
100 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
101 h3("Settings").key("metric:page.title"),
102 spacer(),
103 button("Reset").secondary(),
104 button("Save changes").primary(),
105 ])
106 .key("metric:header")
107 .gap(tokens::SPACE_3)
108 .height(Size::Fixed(56.0))
109 .padding(Sides::xy(tokens::SPACE_4, 0.0))
110 .align(Align::Center)
111 .stroke(tokens::BORDER)
112}More examples
194fn dashboard_header() -> El {
195 row([
196 icon_button("menu").ghost(),
197 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
198 h3("Documents").key("metric:page.title"),
199 spacer(),
200 text_input("Search...", &Selection::default(), "dashboard-search")
201 .key("metric:command.input")
202 .width(Size::Fixed(260.0)),
203 icon_button("plus").ghost(),
204 icon_button("bell").ghost(),
205 ])
206 .key("metric:header")
207 .gap(tokens::SPACE_3)
208 .height(Size::Fixed(56.0))
209 .padding(Sides::xy(tokens::SPACE_4, 0.0))
210 .align(Align::Center)
211 .stroke(tokens::BORDER)
212}9fn modal_fixture() -> El {
10 stack([
11 column([
12 h1("Account"),
13 titled_card(
14 "Profile",
15 [
16 row([text("Email"), spacer(), text("user@example.com").muted()]),
17 row([text("Plan"), spacer(), badge("Pro").info()]),
18 ],
19 ),
20 titled_card(
21 "Danger zone",
22 [row([
23 column([
24 text("Delete account").bold(),
25 text("Remove this account and all associated data.")
26 .muted()
27 .small(),
28 ])
29 .gap(tokens::SPACE_1)
30 .align(Align::Start)
31 .width(Size::Hug),
32 spacer(),
33 button("Delete").destructive().key("open-delete"),
34 ])],
35 ),
36 ])
37 .gap(tokens::SPACE_4)
38 .padding(tokens::SPACE_7),
39 modal(
40 "delete-account",
41 "Delete account?",
42 [
43 text("Permanent action. Export data first.").muted(),
44 row([
45 spacer(),
46 button("Cancel").ghost().key("cancel-delete"),
47 button("Delete").destructive().key("confirm-delete"),
48 ])
49 .gap(tokens::SPACE_2),
50 ],
51 ),
52 ])
53}314fn component_section() -> El {
315 card([
316 card_header([
317 card_title("Stock widgets"),
318 card_description("The same palette applied to regular component constructors."),
319 ]),
320 card_content([
321 row([
322 button("Primary").primary(),
323 button("Secondary").secondary(),
324 button("Outline").outline(),
325 button("Ghost").ghost(),
326 ])
327 .gap(tokens::SPACE_2)
328 .align(Align::Center),
329 row([
330 badge("success").success(),
331 badge("warning").warning(),
332 badge("destructive").destructive(),
333 badge("info").info(),
334 badge("muted").muted(),
335 ])
336 .gap(tokens::SPACE_2)
337 .align(Align::Center),
338 row([
339 text_input("palette search", &Selection::default(), "palette:search")
340 .width(Size::Fill(1.0)),
341 button_with_icon("settings", "Tune").secondary(),
342 ])
343 .gap(tokens::SPACE_2)
344 .align(Align::Center),
345 row([
346 surface_sample("Card", tokens::CARD),
347 surface_sample("Muted", tokens::MUTED),
348 surface_sample("Popover", tokens::POPOVER),
349 ])
350 .gap(tokens::SPACE_3)
351 .align(Align::Stretch),
352 ])
353 .gap(tokens::SPACE_4),
354 ])
355 .height(Size::Hug)
356}16fn settings() -> El {
17 column([
18 h1("Settings"),
19 titled_card(
20 "Account",
21 [
22 row([text("Email"), spacer(), text("user@example.com").muted()]),
23 row([
24 text("Two-factor authentication"),
25 spacer(),
26 badge("Enabled").success(),
27 ]),
28 row([
29 text("Recovery codes"),
30 spacer(),
31 button("Generate").secondary(),
32 ]),
33 ],
34 ),
35 titled_card(
36 "Appearance",
37 [
38 row([text("Theme"), spacer(), button("Dark").secondary()]),
39 row([text("Compact mode"), spacer(), badge("Off").muted()]),
40 row([text("Font size"), spacer(), text("14")]),
41 ],
42 ),
43 titled_card(
44 "Danger zone",
45 [row([
46 column([
47 text("Delete account").bold(),
48 text("Permanently remove your account and all data.")
49 .muted()
50 .small(),
51 ])
52 .gap(tokens::SPACE_1)
53 .align(Align::Start)
54 .width(Size::Hug),
55 spacer(),
56 button("Delete").destructive(),
57 ])],
58 ),
59 row([spacer(), button("Cancel").ghost(), button("Save").primary()]),
60 ])
61 .gap(tokens::SPACE_4)
62 .padding(tokens::SPACE_7)
63}Sourcepub fn outline(self) -> Self
pub fn outline(self) -> Self
Outline-only style: no fill, prominent border.
Examples found in repository?
314fn component_section() -> El {
315 card([
316 card_header([
317 card_title("Stock widgets"),
318 card_description("The same palette applied to regular component constructors."),
319 ]),
320 card_content([
321 row([
322 button("Primary").primary(),
323 button("Secondary").secondary(),
324 button("Outline").outline(),
325 button("Ghost").ghost(),
326 ])
327 .gap(tokens::SPACE_2)
328 .align(Align::Center),
329 row([
330 badge("success").success(),
331 badge("warning").warning(),
332 badge("destructive").destructive(),
333 badge("info").info(),
334 badge("muted").muted(),
335 ])
336 .gap(tokens::SPACE_2)
337 .align(Align::Center),
338 row([
339 text_input("palette search", &Selection::default(), "palette:search")
340 .width(Size::Fill(1.0)),
341 button_with_icon("settings", "Tune").secondary(),
342 ])
343 .gap(tokens::SPACE_2)
344 .align(Align::Center),
345 row([
346 surface_sample("Card", tokens::CARD),
347 surface_sample("Muted", tokens::MUTED),
348 surface_sample("Popover", tokens::POPOVER),
349 ])
350 .gap(tokens::SPACE_3)
351 .align(Align::Stretch),
352 ])
353 .gap(tokens::SPACE_4),
354 ])
355 .height(Size::Hug)
356}Sourcepub fn muted(self) -> Self
pub fn muted(self) -> Self
Muted/neutral emphasis. On surface profiles this swaps to a neutral background; on text-only profiles it switches the text color to muted-foreground.
Examples found in repository?
20fn list_rows() -> Vec<El> {
21 (0..40)
22 .map(|i| {
23 row([
24 text(format!("{i:02}.")).mono().muted(),
25 text(format!("scrollable list item {i}")),
26 ])
27 .gap(tokens::SPACE_2)
28 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_1))
29 .height(Size::Fixed(28.0))
30 .align(Align::Center)
31 })
32 .collect()
33}
34
35fn fixture() -> El {
36 column([
37 h2("Scrollbar"),
38 text("scroll() and virtual_list() show a draggable thumb by default.").muted(),
39 row([
40 // 1) scroll() — default-on scrollbar.
41 column([
42 text("scroll() — default").bold(),
43 scroll(list_rows())
44 .height(Size::Fixed(240.0))
45 .padding(tokens::SPACE_2)
46 .stroke(tokens::BORDER)
47 .stroke_width(1.0)
48 .radius(tokens::RADIUS_MD),
49 ])
50 .gap(tokens::SPACE_2)
51 .width(Size::Fill(1.0))
52 .height(Size::Hug),
53 // 2) virtual_list — thumb scales to content size.
54 column([
55 text("virtual_list(200, 28)").bold(),
56 virtual_list(200, 28.0, |i| {
57 row([
58 text(format!("{i:03}")).mono().muted(),
59 text(format!("row {i}")),
60 ])
61 .gap(tokens::SPACE_2)
62 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_1))
63 .height(Size::Fixed(28.0))
64 .align(Align::Center)
65 })
66 .height(Size::Fixed(240.0))
67 .padding(tokens::SPACE_2)
68 .stroke(tokens::BORDER)
69 .stroke_width(1.0)
70 .radius(tokens::RADIUS_MD),
71 ])
72 .gap(tokens::SPACE_2)
73 .width(Size::Fill(1.0))
74 .height(Size::Hug),
75 // 3) Opt-out: same content, no thumb.
76 column([
77 text("scroll().no_scrollbar()").bold(),
78 scroll(list_rows())
79 .no_scrollbar()
80 .height(Size::Fixed(240.0))
81 .padding(tokens::SPACE_2)
82 .stroke(tokens::BORDER)
83 .stroke_width(1.0)
84 .radius(tokens::RADIUS_MD),
85 ])
86 .gap(tokens::SPACE_2)
87 .width(Size::Fill(1.0))
88 .height(Size::Hug),
89 ])
90 .gap(tokens::SPACE_4)
91 .width(Size::Fill(1.0)),
92 ])
93 .gap(tokens::SPACE_4)
94 .padding(tokens::SPACE_7)
95}More examples
68fn build_row(c: &FakeCommit, idx: usize, selected: bool) -> El {
69 row([
70 graph_cell(c.lane, selected),
71 text(c.sha).mono().muted(),
72 text(c.subject),
73 spacer(),
74 text(format!("{} · {}", c.author, c.when)).muted(),
75 ])
76 .key(format!("commit-{idx}"))
77 .gap(tokens::SPACE_3)
78 .padding(Sides::xy(tokens::SPACE_2, 0.0))
79 .height(Size::Fixed(ROW_HEIGHT))
80 .align(Align::Center)
81}
82
83fn fixture() -> El {
84 #[rustfmt::skip]
85 let commits = [
86 FakeCommit { sha: "8a3f1c9", subject: "fix race condition in scheduler", author: "ada", when: "12m", lane: 0 },
87 FakeCommit { sha: "1b07d4e", subject: "tweak token tooltip wording", author: "linus", when: "1h", lane: 0 },
88 FakeCommit { sha: "9f2e4a1", subject: "wire avatar fallback identicon", author: "joelle", when: "3h", lane: 1 },
89 FakeCommit { sha: "44ab8d2", subject: "diff: word-level highlight pass", author: "raphael", when: "5h", lane: 1 },
90 FakeCommit { sha: "61c0fe7", subject: "ci: bump rust toolchain to 1.85", author: "mei", when: "7h", lane: 2 },
91 FakeCommit { sha: "a90215b", subject: "switch logging to env_logger", author: "isabel", when: "1d", lane: 2 },
92 FakeCommit { sha: "0d7e3c4", subject: "drop unused commit_detail cache", author: "noor", when: "1d", lane: 1 },
93 FakeCommit { sha: "33b2118", subject: "context-menu spacing pass", author: "kira", when: "2d", lane: 3 },
94 ];
95 let selected_idx = 3;
96 let rows = commits
97 .iter()
98 .enumerate()
99 .map(|(i, c)| build_row(c, i, i == selected_idx))
100 .collect::<Vec<_>>();
101
102 column([
103 h2("Custom-painted commit graph"),
104 text("8 commits · custom shader paints lane line + circle node").muted(),
105 column(rows).gap(0.0),
106 ])
107 .padding(tokens::SPACE_4)
108 .gap(tokens::SPACE_2)
109}18fn build_row(i: usize) -> El {
19 let badge_el = match i % 5 {
20 0 => badge("info").muted(),
21 1 => badge("warn").warning(),
22 2 => badge("ok").success(),
23 3 => badge("err").destructive(),
24 _ => spacer(),
25 };
26 row([
27 text(format!("#{i:05}")).mono(),
28 spacer(),
29 text(format!("entry {i}")),
30 spacer(),
31 badge_el,
32 ])
33 .key(format!("row-{i}"))
34 .gap(tokens::SPACE_3)
35 .padding(Sides::xy(tokens::SPACE_3, tokens::SPACE_2))
36 .height(Size::Fixed(ROW_HEIGHT))
37}
38
39fn fixture() -> El {
40 column([
41 h1("Virtualized list"),
42 paragraph(format!(
43 "{ROW_COUNT} rows × {ROW_HEIGHT}px in a windowed viewport. \
44 Only the rows intersecting the viewport are realized — see \
45 `tree.txt` for proof."
46 ))
47 .muted(),
48 virtual_list(ROW_COUNT, ROW_HEIGHT, build_row)
49 .key("entries")
50 .height(Size::Fill(1.0)),
51 ])
52 .gap(tokens::SPACE_4)
53 .padding(tokens::SPACE_7)
54}164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}44fn sidebar() -> El {
45 column([
46 column([h2("Aetna"), text("calibration").muted()])
47 .key("metric:sidebar.brand")
48 .gap(tokens::SPACE_1)
49 .height(Size::Hug),
50 spacer().height(Size::Fixed(tokens::SPACE_4)),
51 nav_item("01", "Overview", true),
52 nav_item("02", "Commands", false),
53 nav_item("03", "Tables", false),
54 nav_item("04", "Forms", false),
55 spacer(),
56 badge("dark theme").muted(),
57 ])
58 .gap(tokens::SPACE_2)
59 .padding(tokens::SPACE_5)
60 .key("metric:sidebar")
61 .width(Size::Fixed(220.0))
62 .height(Size::Fill(1.0))
63 .fill(tokens::CARD)
64 .stroke(tokens::BORDER)
65}
66
67fn nav_item(icon: &'static str, label: &'static str, selected: bool) -> El {
68 let mut item = row([
69 icon_cell(icon),
70 text(label)
71 .font_weight(FontWeight::Medium)
72 .ellipsis()
73 .width(Size::Fill(1.0)),
74 ])
75 .key(if selected {
76 "metric:sidebar.nav.row".to_string()
77 } else {
78 format!("nav-{label}")
79 })
80 .metrics_role(MetricsRole::ListItem)
81 .gap(tokens::SPACE_3)
82 .padding(Sides::xy(tokens::SPACE_2, 0.0))
83 .height(Size::Fixed(40.0))
84 .align(Align::Center)
85 .focusable();
86
87 if selected {
88 item = item.current();
89 }
90
91 item
92}
93
94fn main_panel() -> El {
95 column([
96 toolbar(),
97 column([
98 row([
99 kpi_card("Latency", "42 ms", "-18%", true),
100 kpi_card("Runs", "1,284", "+12%", true),
101 kpi_card("Errors", "7", "+2", false),
102 ])
103 .gap(tokens::SPACE_4),
104 row([table_card(), command_card()])
105 .gap(tokens::SPACE_4)
106 .height(Size::Fill(1.0))
107 .align(Align::Stretch),
108 ])
109 .gap(tokens::SPACE_4)
110 .height(Size::Fill(1.0))
111 .align(Align::Stretch),
112 ])
113 .padding(tokens::SPACE_7)
114 .gap(tokens::SPACE_2)
115 .width(Size::Fill(1.0))
116 .height(Size::Fill(1.0))
117}
118
119fn toolbar() -> El {
120 row([
121 column([
122 h1("Polish calibration").key("metric:page.title"),
123 text("A representative app surface for default tuning.")
124 .muted()
125 .key("metric:page.subtitle"),
126 ])
127 .gap(tokens::SPACE_2)
128 .height(Size::Hug),
129 spacer(),
130 button_with_icon("search", "Preview")
131 .secondary()
132 .key("metric:action.secondary"),
133 button_with_icon("upload", "Publish")
134 .primary()
135 .key("metric:action.primary"),
136 ])
137 .key("metric:header")
138 .gap(tokens::SPACE_4)
139 .height(Size::Hug)
140 .align(Align::Start)
141}
142
143fn kpi_card(label: &'static str, value: &'static str, delta: &'static str, positive: bool) -> El {
144 let delta_badge = if positive {
145 badge(delta).success()
146 } else {
147 badge(delta).destructive()
148 };
149 let delta_badge = if label == "Latency" {
150 delta_badge.key("metric:kpi.badge")
151 } else {
152 delta_badge
153 };
154 let value_text = h2(value).display();
155 let value_text = if label == "Latency" {
156 value_text.key("metric:kpi.value")
157 } else {
158 value_text
159 };
160 card([
161 card_header([card_title(label)]),
162 card_content([
163 row([value_text, spacer(), delta_badge]).align(Align::Center),
164 text(if positive {
165 "Moving in the expected direction"
166 } else {
167 "Needs visual attention"
168 })
169 .muted(),
170 ])
171 .gap(tokens::SPACE_6),
172 ])
173 .key(if label == "Latency" {
174 "metric:kpi.card"
175 } else {
176 label
177 })
178 .width(Size::Fill(1.0))
179}
180
181fn table_card() -> El {
182 card([
183 card_header([card_title("Reference rows")]),
184 card_content([table([
185 table_header([table_row([
186 table_head("Status").width(Size::Fixed(86.0)),
187 table_head("Surface").width(Size::Fill(1.0)),
188 table_head("Owner").width(Size::Fixed(110.0)),
189 table_head("State").width(Size::Fixed(86.0)),
190 ])
191 .key("metric:table.header")]),
192 divider(),
193 table_body([
194 data_row("OK", "Settings card", "core", "selected", true, "success"),
195 data_row(
196 "WARN",
197 "Command palette density",
198 "widgets",
199 "needs work",
200 false,
201 "warning",
202 ),
203 data_row(
204 "ERR",
205 "Disabled and invalid states",
206 "style",
207 "missing",
208 false,
209 "destructive",
210 ),
211 data_row(
212 "INFO",
213 "Token resolution",
214 "theme",
215 "planned",
216 false,
217 "info",
218 ),
219 data_row(
220 "OK",
221 "Popover elevation",
222 "shader",
223 "queued",
224 false,
225 "success",
226 ),
227 ])
228 .gap(tokens::SPACE_1)
229 .width(Size::Fill(1.0)),
230 ])]),
231 ])
232 .key("metric:table.card")
233 .width(Size::Fill(1.2))
234 .height(Size::Fill(1.0))
235}
236
237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}15fn scroll_list_fixture() -> El {
16 let rows: Vec<El> = (0..20)
17 .map(|i| {
18 row([
19 badge(format!("#{i}")).info(),
20 text(format!("Notification {i}")).bold(),
21 spacer(),
22 text(format!("{}m ago", i + 1)).muted(),
23 ])
24 .gap(tokens::SPACE_2)
25 .height(Size::Fixed(44.0))
26 .padding(Sides::xy(tokens::SPACE_3, tokens::SPACE_2))
27 })
28 .collect();
29
30 let list = scroll(rows)
31 .key("notifications")
32 .height(Size::Fixed(420.0))
33 .padding(tokens::SPACE_2);
34
35 column([
36 h2("Notifications"),
37 text("Roll the wheel inside the panel to scroll. The content is taller than the viewport.")
38 .muted(),
39 list,
40 ])
41 .gap(tokens::SPACE_4)
42 .padding(tokens::SPACE_7)
43}Sourcepub fn selected(self) -> Self
pub fn selected(self) -> Self
Selected row/item treatment. Use for the item that is selected inside a collection, not for transient keyboard focus.
Examples found in repository?
237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}Sourcepub fn current(self) -> Self
pub fn current(self) -> Self
Current navigation/page treatment. Slightly quieter than
Self::selected so nav chrome does not compete with content.
Examples found in repository?
67fn nav_item(icon: &'static str, label: &'static str, selected: bool) -> El {
68 let mut item = row([
69 icon_cell(icon),
70 text(label)
71 .font_weight(FontWeight::Medium)
72 .ellipsis()
73 .width(Size::Fill(1.0)),
74 ])
75 .key(if selected {
76 "metric:sidebar.nav.row".to_string()
77 } else {
78 format!("nav-{label}")
79 })
80 .metrics_role(MetricsRole::ListItem)
81 .gap(tokens::SPACE_3)
82 .padding(Sides::xy(tokens::SPACE_2, 0.0))
83 .height(Size::Fixed(40.0))
84 .align(Align::Center)
85 .focusable();
86
87 if selected {
88 item = item.current();
89 }
90
91 item
92}More examples
134fn settings_nav_item(label: &'static str, selected: bool) -> El {
135 let mut item = row([
136 El::new(Kind::Custom("nav-dot"))
137 .fill(tokens::MUTED_FOREGROUND)
138 .radius(tokens::RADIUS_PILL)
139 .width(Size::Fixed(6.0))
140 .height(Size::Fixed(6.0)),
141 text(label)
142 .font_weight(FontWeight::Medium)
143 .ellipsis()
144 .width(Size::Fill(1.0)),
145 ])
146 .key(if selected {
147 "metric:settings.nav.row".to_string()
148 } else {
149 format!("settings-nav-{label}")
150 })
151 .metrics_role(MetricsRole::ListItem)
152 .align(Align::Center)
153 .focusable();
154
155 if selected {
156 item = item.current();
157 } else {
158 item = item.color(tokens::MUTED_FOREGROUND);
159 }
160
161 item
162}
163
164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}
185
186fn profile_card() -> El {
187 card([
188 card_header([
189 card_title("Profile"),
190 card_description("This information appears in audit logs and shared documents."),
191 ]),
192 card_content([form([
193 row([
194 setting_field("Display name", "Alicia Koch", "display-name"),
195 setting_field("Email", "alicia@acme.co", "email"),
196 ])
197 .gap(tokens::SPACE_3),
198 row([
199 setting_select("Role", "Workspace admin", "role"),
200 setting_select("Region", "US East", "region"),
201 ])
202 .gap(tokens::SPACE_3),
203 ])]),
204 ])
205 .key("metric:profile.card")
206}
207
208fn setting_field(label: &'static str, value: &'static str, key: &'static str) -> El {
209 form_item([
210 form_label(label),
211 form_control(
212 text_input(value, &Selection::caret(key, value.len()), key).key(
213 if key == "display-name" {
214 "metric:form.input"
215 } else {
216 key
217 },
218 ),
219 ),
220 ])
221 .width(Size::Fill(1.0))
222}
223
224fn setting_select(label: &'static str, value: &'static str, key: &'static str) -> El {
225 form_item([form_label(label), form_control(select_trigger(key, value))]).width(Size::Fill(1.0))
226}
227
228fn preferences_card() -> El {
229 card([
230 card_header([
231 card_title("Preferences"),
232 card_description("Defaults used when creating new dashboards and exports."),
233 ]),
234 card_content([column([
235 preference_row(
236 "Compact navigation",
237 "Use tighter rows in the sidebar and command menus.",
238 switch(true).key("compact-navigation"),
239 ),
240 divider(),
241 preference_row(
242 "Email summaries",
243 "Send a daily digest when documents change.",
244 switch(false).key("email-summaries"),
245 ),
246 divider(),
247 preference_row(
248 "Require approval",
249 "Route external sharing through an owner review.",
250 checkbox(true).key("approval-required"),
251 ),
252 ])
253 .gap(0.0)
254 .width(Size::Fill(1.0))])
255 .padding(0.0),
256 ])
257 .key("metric:preferences.card")
258}
259
260fn preference_row(title: &'static str, description: &'static str, control: El) -> El {
261 row([
262 column([
263 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
264 text(description)
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0))
271 .height(Size::Hug),
272 control,
273 ])
274 .key(if title == "Compact navigation" {
275 "metric:preference.row".to_string()
276 } else {
277 format!("preference-{title}")
278 })
279 .metrics_role(MetricsRole::PreferenceRow)
280 .gap(tokens::SPACE_4)
281 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_3))
282 .align(Align::Center)
283}
284
285fn settings_aside() -> El {
286 column([security_card(), scale_card()])
287 .gap(tokens::SPACE_4)
288 .width(Size::Fixed(300.0))
289 .height(Size::Fill(1.0))
290}
291
292fn security_card() -> El {
293 card([
294 card_header([
295 card_title("Security"),
296 card_description("Two-factor authentication is enabled for all privileged users."),
297 ]),
298 card_content([
299 compact_stat("Passkeys", "2 registered", badge("On").success()),
300 compact_stat("Sessions", "3 active", button("Review").secondary()),
301 ]),
302 ])
303 .width(Size::Fill(1.0))
304}
305
306fn scale_card() -> El {
307 card([
308 card_header([
309 card_title("Interface scale"),
310 card_description("Reference captures keep browser zoom fixed and vary root UI scale."),
311 ]),
312 card_content([
313 row([text("Dense").caption(), spacer(), text("Default").caption()]),
314 slider(0.66, tokens::PRIMARY)
315 .key("interface-scale")
316 .width(Size::Fill(1.0)),
317 ]),
318 ])
319 .width(Size::Fill(1.0))
320}
321
322fn compact_stat(title: &'static str, detail: &'static str, control: El) -> El {
323 row([
324 column([
325 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
326 text(detail).caption().ellipsis().width(Size::Fill(1.0)),
327 ])
328 .gap(2.0)
329 .width(Size::Fill(1.0))
330 .height(Size::Hug),
331 control,
332 ])
333 .gap(tokens::SPACE_2)
334 .height(Size::Fixed(44.0))
335 .align(Align::Center)
336}
337
338fn section_label(label: &'static str) -> El {
339 text(label)
340 .caption()
341 .height(Size::Fixed(22.0))
342 .padding(Sides::xy(tokens::SPACE_2, 0.0))
343}
344
345fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
346 let mut item = row([
347 icon(icon_name)
348 .color(tokens::MUTED_FOREGROUND)
349 .icon_size(tokens::ICON_SM)
350 .width(Size::Fixed(tokens::ICON_SM)),
351 text(label)
352 .font_weight(FontWeight::Medium)
353 .ellipsis()
354 .width(Size::Fill(1.0)),
355 ])
356 .key(if selected {
357 "metric:sidebar.nav.row".to_string()
358 } else {
359 format!("side-item-{label}")
360 })
361 .metrics_role(MetricsRole::ListItem)
362 .gap(tokens::SPACE_2)
363 .padding(Sides::xy(tokens::SPACE_2, 0.0))
364 .height(Size::Fixed(32.0))
365 .align(Align::Center)
366 .focusable();
367
368 if selected {
369 item = item.current();
370 } else {
371 item = item.color(tokens::MUTED_FOREGROUND);
372 }
373
374 item
375}109fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
110 let mut item = row([
111 icon(icon_name)
112 .color(tokens::MUTED_FOREGROUND)
113 .icon_size(tokens::ICON_SM)
114 .width(Size::Fixed(tokens::ICON_SM)),
115 text(label)
116 .font_weight(FontWeight::Medium)
117 .ellipsis()
118 .width(Size::Fill(1.0)),
119 ])
120 .key(if selected {
121 "metric:sidebar.nav.row".to_string()
122 } else {
123 format!("side-item-{label}")
124 })
125 .metrics_role(MetricsRole::ListItem)
126 .gap(tokens::SPACE_2)
127 .padding(Sides::xy(tokens::SPACE_2, 0.0))
128 .height(Size::Fixed(32.0))
129 .align(Align::Center)
130 .focusable();
131
132 if selected {
133 item = item.current();
134 } else {
135 item = item.color(tokens::MUTED_FOREGROUND);
136 }
137
138 item
139}Sourcepub fn disabled(self) -> Self
pub fn disabled(self) -> Self
Disabled treatment for controls and rows. Also removes the node from focus order and blocks pointer hits on this element.
Examples found in repository?
317fn form_probe() -> El {
318 form([
319 form_item([
320 form_label("Valid input"),
321 form_control(
322 text_input(
323 "Valid input",
324 &Selection::caret("valid-input", 11),
325 "valid-input",
326 )
327 .key("metric:form.input"),
328 ),
329 form_description("Default field spacing and helper text."),
330 ]),
331 form_item([
332 form_label("Invalid input"),
333 form_control(
334 text_input(
335 "Invalid input",
336 &Selection::caret("invalid-input", 13),
337 "invalid-input",
338 )
339 .invalid(),
340 ),
341 form_message("This field needs attention."),
342 ]),
343 row([
344 button("Disabled").secondary().disabled(),
345 button("Loading").primary().loading(),
346 spacer(),
347 ]),
348 ])
349 .padding(tokens::SPACE_3)
350 .fill(tokens::MUTED)
351 .stroke(tokens::BORDER)
352 .radius(tokens::RADIUS_MD)
353}Sourcepub fn invalid(self) -> Self
pub fn invalid(self) -> Self
Invalid/error treatment for inputs, rows, and validation badges.
Examples found in repository?
317fn form_probe() -> El {
318 form([
319 form_item([
320 form_label("Valid input"),
321 form_control(
322 text_input(
323 "Valid input",
324 &Selection::caret("valid-input", 11),
325 "valid-input",
326 )
327 .key("metric:form.input"),
328 ),
329 form_description("Default field spacing and helper text."),
330 ]),
331 form_item([
332 form_label("Invalid input"),
333 form_control(
334 text_input(
335 "Invalid input",
336 &Selection::caret("invalid-input", 13),
337 "invalid-input",
338 )
339 .invalid(),
340 ),
341 form_message("This field needs attention."),
342 ]),
343 row([
344 button("Disabled").secondary().disabled(),
345 button("Loading").primary().loading(),
346 spacer(),
347 ]),
348 ])
349 .padding(tokens::SPACE_3)
350 .fill(tokens::MUTED)
351 .stroke(tokens::BORDER)
352 .radius(tokens::RADIUS_MD)
353}Sourcepub fn loading(self) -> Self
pub fn loading(self) -> Self
Loading treatment for a direct text-bearing node. Container widgets can still use this for opacity even when they do not have their own label text.
Examples found in repository?
317fn form_probe() -> El {
318 form([
319 form_item([
320 form_label("Valid input"),
321 form_control(
322 text_input(
323 "Valid input",
324 &Selection::caret("valid-input", 11),
325 "valid-input",
326 )
327 .key("metric:form.input"),
328 ),
329 form_description("Default field spacing and helper text."),
330 ]),
331 form_item([
332 form_label("Invalid input"),
333 form_control(
334 text_input(
335 "Invalid input",
336 &Selection::caret("invalid-input", 13),
337 "invalid-input",
338 )
339 .invalid(),
340 ),
341 form_message("This field needs attention."),
342 ]),
343 row([
344 button("Disabled").secondary().disabled(),
345 button("Loading").primary().loading(),
346 spacer(),
347 ]),
348 ])
349 .padding(tokens::SPACE_3)
350 .fill(tokens::MUTED)
351 .stroke(tokens::BORDER)
352 .radius(tokens::RADIUS_MD)
353}pub fn text_role(self, role: TextRole) -> Self
Sourcepub fn caption(self) -> Self
pub fn caption(self) -> Self
Examples found in repository?
282fn token_chip(token: TokenDef, palette: &Palette) -> El {
283 let resolved = palette.resolve(token.color);
284 row([
285 El::new(Kind::Custom("palette-swatch"))
286 .at(file!(), line!())
287 .fill(token.color)
288 .stroke(tokens::BORDER)
289 .radius(tokens::RADIUS_SM)
290 .width(Size::Fixed(42.0))
291 .height(Size::Fixed(34.0)),
292 column([
293 text(token.name)
294 .label()
295 .ellipsis()
296 .nowrap_text()
297 .width(Size::Fill(1.0)),
298 mono(rgba_label(resolved)).caption().muted(),
299 ])
300 .gap(0.0)
301 .width(Size::Fill(1.0))
302 .height(Size::Hug),
303 ])
304 .gap(tokens::SPACE_2)
305 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_2))
306 .align(Align::Center)
307 .fill(tokens::CARD)
308 .stroke(tokens::BORDER)
309 .radius(tokens::RADIUS_MD)
310 .width(Size::Fill(1.0))
311 .height(Size::Fixed(54.0))
312}
313
314fn component_section() -> El {
315 card([
316 card_header([
317 card_title("Stock widgets"),
318 card_description("The same palette applied to regular component constructors."),
319 ]),
320 card_content([
321 row([
322 button("Primary").primary(),
323 button("Secondary").secondary(),
324 button("Outline").outline(),
325 button("Ghost").ghost(),
326 ])
327 .gap(tokens::SPACE_2)
328 .align(Align::Center),
329 row([
330 badge("success").success(),
331 badge("warning").warning(),
332 badge("destructive").destructive(),
333 badge("info").info(),
334 badge("muted").muted(),
335 ])
336 .gap(tokens::SPACE_2)
337 .align(Align::Center),
338 row([
339 text_input("palette search", &Selection::default(), "palette:search")
340 .width(Size::Fill(1.0)),
341 button_with_icon("settings", "Tune").secondary(),
342 ])
343 .gap(tokens::SPACE_2)
344 .align(Align::Center),
345 row([
346 surface_sample("Card", tokens::CARD),
347 surface_sample("Muted", tokens::MUTED),
348 surface_sample("Popover", tokens::POPOVER),
349 ])
350 .gap(tokens::SPACE_3)
351 .align(Align::Stretch),
352 ])
353 .gap(tokens::SPACE_4),
354 ])
355 .height(Size::Hug)
356}
357
358fn surface_sample(title: &'static str, fill: Color) -> El {
359 column([
360 text(title).label(),
361 text("surface sample").caption().muted(),
362 ])
363 .gap(tokens::SPACE_1)
364 .padding(tokens::SPACE_3)
365 .fill(fill)
366 .stroke(tokens::BORDER)
367 .radius(tokens::RADIUS_MD)
368 .width(Size::Fill(1.0))
369 .height(Size::Fixed(76.0))
370}More examples
237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}
287
288fn command_card() -> El {
289 card([
290 card_header([card_title("Command surface")]),
291 card_content([
292 text_input(
293 "Search commands...",
294 &Selection::default(),
295 "command-search",
296 )
297 .key("metric:command.input")
298 .width(Size::Fill(1.0)),
299 popover_panel([
300 command_row("git-branch", "New branch", "Ctrl+B").key("metric:command.row"),
301 command_row("git-commit", "Commit staged files", "Ctrl+Enter")
302 .key("command-row-commit"),
303 command_row("refresh-cw", "Refresh repository", "Ctrl+R")
304 .key("command-row-refresh"),
305 command_row("alert-circle", "Force push", "Danger").key("command-row-force"),
306 ])
307 .width(Size::Fill(1.0)),
308 scroll([form_probe()]).key("form-probe-scroll"),
309 ])
310 .height(Size::Fill(1.0)),
311 ])
312 .key("metric:command.card")
313 .width(Size::Fill(0.8))
314 .height(Size::Fill(1.0))
315}
316
317fn form_probe() -> El {
318 form([
319 form_item([
320 form_label("Valid input"),
321 form_control(
322 text_input(
323 "Valid input",
324 &Selection::caret("valid-input", 11),
325 "valid-input",
326 )
327 .key("metric:form.input"),
328 ),
329 form_description("Default field spacing and helper text."),
330 ]),
331 form_item([
332 form_label("Invalid input"),
333 form_control(
334 text_input(
335 "Invalid input",
336 &Selection::caret("invalid-input", 13),
337 "invalid-input",
338 )
339 .invalid(),
340 ),
341 form_message("This field needs attention."),
342 ]),
343 row([
344 button("Disabled").secondary().disabled(),
345 button("Loading").primary().loading(),
346 spacer(),
347 ]),
348 ])
349 .padding(tokens::SPACE_3)
350 .fill(tokens::MUTED)
351 .stroke(tokens::BORDER)
352 .radius(tokens::RADIUS_MD)
353}
354
355fn icon_cell(label: &'static str) -> El {
356 El::new(Kind::Custom("icon_cell"))
357 .style_profile(StyleProfile::Surface)
358 .text(label)
359 .text_align(TextAlign::Center)
360 .caption()
361 .font_weight(FontWeight::Semibold)
362 .fill(tokens::MUTED)
363 .stroke(tokens::BORDER)
364 .radius(tokens::RADIUS_SM)
365 .width(Size::Fixed(26.0))
366 .height(Size::Fixed(26.0))
367}41fn settings_sidebar() -> El {
42 column([
43 row([
44 icon_slot("settings"),
45 column([
46 text("Workspace")
47 .semibold()
48 .ellipsis()
49 .width(Size::Fill(1.0)),
50 text("Settings").caption().ellipsis().width(Size::Fill(1.0)),
51 ])
52 .gap(2.0)
53 .width(Size::Fill(1.0))
54 .height(Size::Hug),
55 ])
56 .gap(tokens::SPACE_2)
57 .height(Size::Fixed(44.0))
58 .align(Align::Center),
59 section_label("Personal"),
60 side_item("users", "Profile", false),
61 side_item("settings", "Account", true),
62 side_item("alert-circle", "Security", false),
63 side_item("bell", "Notifications", false),
64 spacer().height(Size::Fixed(tokens::SPACE_4)),
65 section_label("Workspace"),
66 side_item("file-text", "Billing", false),
67 side_item("bar-chart", "Appearance", false),
68 side_item("activity", "Integrations", false),
69 spacer(),
70 column([text("Changes sync after save.").caption().wrap_text()])
71 .padding(tokens::SPACE_2)
72 .fill(tokens::MUTED)
73 .radius(tokens::RADIUS_MD),
74 ])
75 .gap(tokens::SPACE_2)
76 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
77 .key("metric:sidebar")
78 .width(Size::Fixed(244.0))
79 .height(Size::Fill(1.0))
80 .fill(tokens::CARD)
81 .stroke(tokens::BORDER)
82}
83
84fn settings_main() -> El {
85 column([
86 settings_header(),
87 row([settings_nav_card(), settings_body(), settings_aside()])
88 .gap(tokens::SPACE_4)
89 .padding(tokens::SPACE_4)
90 .height(Size::Fill(1.0))
91 .align(Align::Stretch),
92 ])
93 .width(Size::Fill(1.0))
94 .height(Size::Fill(1.0))
95}
96
97fn settings_header() -> El {
98 row([
99 icon_button("menu").ghost(),
100 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
101 h3("Settings").key("metric:page.title"),
102 spacer(),
103 button("Reset").secondary(),
104 button("Save changes").primary(),
105 ])
106 .key("metric:header")
107 .gap(tokens::SPACE_3)
108 .height(Size::Fixed(56.0))
109 .padding(Sides::xy(tokens::SPACE_4, 0.0))
110 .align(Align::Center)
111 .stroke(tokens::BORDER)
112}
113
114fn settings_nav_card() -> El {
115 column([
116 settings_nav_item("Account", true),
117 settings_nav_item("Security", false),
118 settings_nav_item("Notifications", false),
119 settings_nav_item("Appearance", false),
120 settings_nav_item("Billing", false),
121 ])
122 .gap(tokens::SPACE_1)
123 .padding(tokens::SPACE_1)
124 .width(Size::Fixed(220.0))
125 .height(Size::Fill(1.0))
126 .style_profile(StyleProfile::Surface)
127 .surface_role(SurfaceRole::Panel)
128 .fill(tokens::CARD)
129 .stroke(tokens::BORDER)
130 .radius(tokens::RADIUS_MD)
131 .shadow(tokens::SHADOW_MD)
132}
133
134fn settings_nav_item(label: &'static str, selected: bool) -> El {
135 let mut item = row([
136 El::new(Kind::Custom("nav-dot"))
137 .fill(tokens::MUTED_FOREGROUND)
138 .radius(tokens::RADIUS_PILL)
139 .width(Size::Fixed(6.0))
140 .height(Size::Fixed(6.0)),
141 text(label)
142 .font_weight(FontWeight::Medium)
143 .ellipsis()
144 .width(Size::Fill(1.0)),
145 ])
146 .key(if selected {
147 "metric:settings.nav.row".to_string()
148 } else {
149 format!("settings-nav-{label}")
150 })
151 .metrics_role(MetricsRole::ListItem)
152 .align(Align::Center)
153 .focusable();
154
155 if selected {
156 item = item.current();
157 } else {
158 item = item.color(tokens::MUTED_FOREGROUND);
159 }
160
161 item
162}
163
164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}
185
186fn profile_card() -> El {
187 card([
188 card_header([
189 card_title("Profile"),
190 card_description("This information appears in audit logs and shared documents."),
191 ]),
192 card_content([form([
193 row([
194 setting_field("Display name", "Alicia Koch", "display-name"),
195 setting_field("Email", "alicia@acme.co", "email"),
196 ])
197 .gap(tokens::SPACE_3),
198 row([
199 setting_select("Role", "Workspace admin", "role"),
200 setting_select("Region", "US East", "region"),
201 ])
202 .gap(tokens::SPACE_3),
203 ])]),
204 ])
205 .key("metric:profile.card")
206}
207
208fn setting_field(label: &'static str, value: &'static str, key: &'static str) -> El {
209 form_item([
210 form_label(label),
211 form_control(
212 text_input(value, &Selection::caret(key, value.len()), key).key(
213 if key == "display-name" {
214 "metric:form.input"
215 } else {
216 key
217 },
218 ),
219 ),
220 ])
221 .width(Size::Fill(1.0))
222}
223
224fn setting_select(label: &'static str, value: &'static str, key: &'static str) -> El {
225 form_item([form_label(label), form_control(select_trigger(key, value))]).width(Size::Fill(1.0))
226}
227
228fn preferences_card() -> El {
229 card([
230 card_header([
231 card_title("Preferences"),
232 card_description("Defaults used when creating new dashboards and exports."),
233 ]),
234 card_content([column([
235 preference_row(
236 "Compact navigation",
237 "Use tighter rows in the sidebar and command menus.",
238 switch(true).key("compact-navigation"),
239 ),
240 divider(),
241 preference_row(
242 "Email summaries",
243 "Send a daily digest when documents change.",
244 switch(false).key("email-summaries"),
245 ),
246 divider(),
247 preference_row(
248 "Require approval",
249 "Route external sharing through an owner review.",
250 checkbox(true).key("approval-required"),
251 ),
252 ])
253 .gap(0.0)
254 .width(Size::Fill(1.0))])
255 .padding(0.0),
256 ])
257 .key("metric:preferences.card")
258}
259
260fn preference_row(title: &'static str, description: &'static str, control: El) -> El {
261 row([
262 column([
263 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
264 text(description)
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0))
271 .height(Size::Hug),
272 control,
273 ])
274 .key(if title == "Compact navigation" {
275 "metric:preference.row".to_string()
276 } else {
277 format!("preference-{title}")
278 })
279 .metrics_role(MetricsRole::PreferenceRow)
280 .gap(tokens::SPACE_4)
281 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_3))
282 .align(Align::Center)
283}
284
285fn settings_aside() -> El {
286 column([security_card(), scale_card()])
287 .gap(tokens::SPACE_4)
288 .width(Size::Fixed(300.0))
289 .height(Size::Fill(1.0))
290}
291
292fn security_card() -> El {
293 card([
294 card_header([
295 card_title("Security"),
296 card_description("Two-factor authentication is enabled for all privileged users."),
297 ]),
298 card_content([
299 compact_stat("Passkeys", "2 registered", badge("On").success()),
300 compact_stat("Sessions", "3 active", button("Review").secondary()),
301 ]),
302 ])
303 .width(Size::Fill(1.0))
304}
305
306fn scale_card() -> El {
307 card([
308 card_header([
309 card_title("Interface scale"),
310 card_description("Reference captures keep browser zoom fixed and vary root UI scale."),
311 ]),
312 card_content([
313 row([text("Dense").caption(), spacer(), text("Default").caption()]),
314 slider(0.66, tokens::PRIMARY)
315 .key("interface-scale")
316 .width(Size::Fill(1.0)),
317 ]),
318 ])
319 .width(Size::Fill(1.0))
320}
321
322fn compact_stat(title: &'static str, detail: &'static str, control: El) -> El {
323 row([
324 column([
325 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
326 text(detail).caption().ellipsis().width(Size::Fill(1.0)),
327 ])
328 .gap(2.0)
329 .width(Size::Fill(1.0))
330 .height(Size::Hug),
331 control,
332 ])
333 .gap(tokens::SPACE_2)
334 .height(Size::Fixed(44.0))
335 .align(Align::Center)
336}
337
338fn section_label(label: &'static str) -> El {
339 text(label)
340 .caption()
341 .height(Size::Fixed(22.0))
342 .padding(Sides::xy(tokens::SPACE_2, 0.0))
343}41fn dashboard_sidebar() -> El {
42 column([
43 row([
44 icon_cell("A"),
45 column([
46 text("Acme Inc.")
47 .semibold()
48 .ellipsis()
49 .width(Size::Fill(1.0)),
50 text("Enterprise")
51 .caption()
52 .ellipsis()
53 .width(Size::Fill(1.0)),
54 ])
55 .gap(2.0)
56 .width(Size::Fill(1.0))
57 .height(Size::Hug),
58 ])
59 .gap(tokens::SPACE_2)
60 .height(Size::Fixed(44.0))
61 .align(Align::Center),
62 section_label("Platform"),
63 side_item("layout-dashboard", "Dashboard", true),
64 side_item("activity", "Lifecycle", false),
65 side_item("bar-chart", "Analytics", false),
66 side_item("folder", "Projects", false),
67 spacer().height(Size::Fixed(tokens::SPACE_4)),
68 section_label("Documents"),
69 side_item("file-text", "Data library", false),
70 side_item("download", "Reports", false),
71 side_item("users", "Team", false),
72 spacer(),
73 row([
74 icon_cell("AK"),
75 column([
76 text("Alicia Koch")
77 .semibold()
78 .ellipsis()
79 .width(Size::Fill(1.0)),
80 text("alicia@example.com")
81 .caption()
82 .ellipsis()
83 .width(Size::Fill(1.0)),
84 ])
85 .gap(2.0)
86 .width(Size::Fill(1.0))
87 .height(Size::Hug),
88 ])
89 .gap(tokens::SPACE_2)
90 .height(Size::Fixed(50.0))
91 .align(Align::Center),
92 ])
93 .gap(tokens::SPACE_2)
94 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
95 .key("metric:sidebar")
96 .width(Size::Fixed(244.0))
97 .height(Size::Fill(1.0))
98 .fill(tokens::CARD)
99 .stroke(tokens::BORDER)
100}
101
102fn section_label(label: &'static str) -> El {
103 text(label)
104 .caption()
105 .height(Size::Fixed(22.0))
106 .padding(Sides::xy(tokens::SPACE_2, 0.0))
107}
108
109fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
110 let mut item = row([
111 icon(icon_name)
112 .color(tokens::MUTED_FOREGROUND)
113 .icon_size(tokens::ICON_SM)
114 .width(Size::Fixed(tokens::ICON_SM)),
115 text(label)
116 .font_weight(FontWeight::Medium)
117 .ellipsis()
118 .width(Size::Fill(1.0)),
119 ])
120 .key(if selected {
121 "metric:sidebar.nav.row".to_string()
122 } else {
123 format!("side-item-{label}")
124 })
125 .metrics_role(MetricsRole::ListItem)
126 .gap(tokens::SPACE_2)
127 .padding(Sides::xy(tokens::SPACE_2, 0.0))
128 .height(Size::Fixed(32.0))
129 .align(Align::Center)
130 .focusable();
131
132 if selected {
133 item = item.current();
134 } else {
135 item = item.color(tokens::MUTED_FOREGROUND);
136 }
137
138 item
139}
140
141fn dashboard_main() -> El {
142 column([
143 dashboard_header(),
144 column([
145 row([
146 metric_card(
147 "bar-chart",
148 "Total Revenue",
149 "$1,250.00",
150 "+12.5%",
151 "Trending up this month",
152 true,
153 ),
154 metric_card(
155 "users",
156 "New Customers",
157 "1,234",
158 "-20%",
159 "Acquisition needs attention",
160 false,
161 ),
162 metric_card(
163 "folder",
164 "Active Accounts",
165 "45,678",
166 "+12.5%",
167 "Strong user retention",
168 true,
169 ),
170 metric_card(
171 "activity",
172 "Growth Rate",
173 "4.5%",
174 "+4.5%",
175 "Meets growth projections",
176 true,
177 ),
178 ])
179 .gap(tokens::SPACE_4),
180 row([chart_card(), sales_card()])
181 .gap(tokens::SPACE_4)
182 .height(Size::Fixed(306.0))
183 .align(Align::Stretch),
184 documents_card(),
185 ])
186 .gap(tokens::SPACE_4)
187 .padding(tokens::SPACE_7)
188 .height(Size::Fill(1.0)),
189 ])
190 .width(Size::Fill(1.0))
191 .height(Size::Fill(1.0))
192}
193
194fn dashboard_header() -> El {
195 row([
196 icon_button("menu").ghost(),
197 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
198 h3("Documents").key("metric:page.title"),
199 spacer(),
200 text_input("Search...", &Selection::default(), "dashboard-search")
201 .key("metric:command.input")
202 .width(Size::Fixed(260.0)),
203 icon_button("plus").ghost(),
204 icon_button("bell").ghost(),
205 ])
206 .key("metric:header")
207 .gap(tokens::SPACE_3)
208 .height(Size::Fixed(56.0))
209 .padding(Sides::xy(tokens::SPACE_4, 0.0))
210 .align(Align::Center)
211 .stroke(tokens::BORDER)
212}
213
214fn metric_card(
215 icon_name: &'static str,
216 title: &'static str,
217 value: &'static str,
218 delta: &'static str,
219 note: &'static str,
220 positive: bool,
221) -> El {
222 let badge = if positive {
223 badge(delta).success()
224 } else {
225 badge(delta).warning()
226 };
227 let badge = if title == "Total Revenue" {
228 badge.key("metric:kpi.badge")
229 } else {
230 badge
231 };
232 let value = if title == "Total Revenue" {
233 h2(value).ellipsis().key("metric:kpi.value")
234 } else {
235 h2(value).ellipsis()
236 };
237 card([card_content([
238 row([
239 row([
240 icon(icon_name)
241 .color(tokens::MUTED_FOREGROUND)
242 .icon_size(tokens::ICON_XS),
243 text(title).muted().ellipsis().width(Size::Fill(1.0)),
244 ])
245 .gap(tokens::SPACE_1)
246 .width(Size::Fill(1.0))
247 .align(Align::Center),
248 badge,
249 ])
250 .gap(tokens::SPACE_2)
251 .align(Align::Center),
252 value,
253 text(note).caption().ellipsis().width(Size::Fill(1.0)),
254 ])
255 .padding(tokens::SPACE_4)
256 .gap(tokens::SPACE_2)])
257 .key(if title == "Total Revenue" {
258 "metric:kpi.card"
259 } else {
260 title
261 })
262 .width(Size::Fill(1.0))
263}
264
265fn chart_card() -> El {
266 card([
267 card_header([
268 card_title("Visitors for the last 6 months"),
269 card_description("Total visitors by channel."),
270 ])
271 .padding(tokens::SPACE_4),
272 card_content([row(chart_bars())
273 .gap(2.0)
274 .height(Size::Fill(1.0))
275 .align(Align::End)])
276 .padding(Sides {
277 left: tokens::SPACE_4,
278 right: tokens::SPACE_4,
279 top: 0.0,
280 bottom: tokens::SPACE_4,
281 })
282 .height(Size::Fill(1.0)),
283 ])
284 .key("metric:chart.card")
285 .width(Size::Fill(1.0))
286 .height(Size::Fill(1.0))
287}
288
289fn chart_bars() -> Vec<El> {
290 [
291 48.0, 72.0, 56.0, 90.0, 64.0, 80.0, 108.0, 84.0, 122.0, 96.0, 136.0, 118.0,
292 ]
293 .into_iter()
294 .flat_map(|height| {
295 [
296 bar(height, tokens::MUTED_FOREGROUND),
297 bar((height - 28.0_f32).max(24.0), tokens::INPUT),
298 ]
299 })
300 .collect()
301}
302
303fn bar(height: f32, color: Color) -> El {
304 El::new(Kind::Custom("chart_bar"))
305 .fill(color)
306 .radius(tokens::RADIUS_SM)
307 .width(Size::Fill(1.0))
308 .height(Size::Fixed(height))
309}
310
311fn sales_card() -> El {
312 card([
313 card_header([
314 card_title("Recent Sales"),
315 card_description("You made 265 sales this month."),
316 ])
317 .padding(tokens::SPACE_4),
318 card_content([
319 sale_row("OM", "Olivia Martin", "olivia@example.com", "+$1,999.00"),
320 sale_row("JL", "Jackson Lee", "jackson@example.com", "+$39.00"),
321 sale_row("IN", "Isabella Nguyen", "isabella@example.com", "+$299.00"),
322 sale_row("WK", "William Kim", "will@example.com", "+$99.00"),
323 ])
324 .gap(tokens::SPACE_2)
325 .padding(Sides {
326 left: tokens::SPACE_4,
327 right: tokens::SPACE_4,
328 top: 0.0,
329 bottom: tokens::SPACE_4,
330 }),
331 ])
332 .key("metric:sales.card")
333 .width(Size::Fixed(330.0))
334 .height(Size::Fill(1.0))
335}
336
337fn sale_row(
338 initials: &'static str,
339 name: &'static str,
340 email: &'static str,
341 amount: &'static str,
342) -> El {
343 row([
344 icon_cell(initials),
345 column([
346 text(name).semibold().ellipsis().width(Size::Fill(1.0)),
347 text(email).caption().ellipsis().width(Size::Fill(1.0)),
348 ])
349 .gap(2.0)
350 .height(Size::Hug)
351 .width(Size::Fill(1.0)),
352 text(amount).label().small(),
353 ])
354 .gap(tokens::SPACE_2)
355 .height(Size::Fixed(42.0))
356 .align(Align::Center)
357}
358
359fn documents_card() -> El {
360 card([
361 card_header([card_title("Documents")]).padding(tokens::SPACE_4),
362 card_content([scroll([table([
363 table_header([table_row([
364 table_head("").width(Size::Fixed(35.0)),
365 table_head("Header").width(Size::Fill(1.8)),
366 table_head("Section Type").width(Size::Fill(1.0)),
367 table_head("Status").width(Size::Fixed(104.0)),
368 table_head("Target").width(Size::Fixed(64.0)),
369 table_head("Limit").width(Size::Fixed(64.0)),
370 table_head("Reviewer").width(Size::Fixed(128.0)),
371 table_head("").width(Size::Fixed(32.0)),
372 ])
373 .padding(Sides::xy(tokens::SPACE_4, 0.0))
374 .key("metric:table.header")]),
375 divider(),
376 table_body([
377 document_row(
378 "Cover page",
379 "Cover page",
380 "In Process",
381 "18",
382 "5",
383 "Eddie Lake",
384 "info",
385 ),
386 document_row(
387 "Table of contents",
388 "Table of contents",
389 "Done",
390 "29",
391 "24",
392 "Eddie Lake",
393 "success",
394 ),
395 ]),
396 ])])
397 .height(Size::Fill(1.0))])
398 .gap(0.0)
399 .padding(0.0)
400 .height(Size::Fill(1.0)),
401 ])
402 .key("metric:table.card")
403 .height(Size::Fill(1.0))
404}
405
406fn document_row(
407 header: &'static str,
408 section: &'static str,
409 status: &'static str,
410 target: &'static str,
411 limit: &'static str,
412 reviewer: &'static str,
413 tone: &'static str,
414) -> El {
415 let status_badge = match tone {
416 "success" => badge(status).success(),
417 _ => badge(status).info(),
418 };
419 table_row([
420 table_utility_cell("::"),
421 table_cell(text(header).label().small()).width(Size::Fill(1.8)),
422 table_cell(text(section).muted()).width(Size::Fill(1.0)),
423 table_cell(status_badge).width(Size::Fixed(104.0)),
424 table_cell(text(target).label().small()).width(Size::Fixed(64.0)),
425 table_cell(text(limit).label().small()).width(Size::Fixed(64.0)),
426 table_cell(text(reviewer).muted()).width(Size::Fixed(128.0)),
427 table_action_cell(),
428 ])
429 .padding(Sides::xy(tokens::SPACE_4, 0.0))
430 .key(if header == "Cover page" {
431 "metric:table.row"
432 } else {
433 header
434 })
435}
436
437fn table_utility_cell(label: &'static str) -> El {
438 table_cell(text(label).muted().center_text()).width(Size::Fixed(35.0))
439}
440
441fn table_action_cell() -> El {
442 stack([icon("more-horizontal")
443 .icon_size(tokens::ICON_SM)
444 .color(tokens::MUTED_FOREGROUND)])
445 .align(Align::Center)
446 .justify(Justify::Center)
447 .width(Size::Fixed(32.0))
448 .height(Size::Hug)
449}
450
451fn icon_cell(label: &'static str) -> El {
452 El::new(Kind::Custom("icon_cell"))
453 .style_profile(StyleProfile::Surface)
454 .text(label)
455 .text_align(TextAlign::Center)
456 .caption()
457 .font_weight(FontWeight::Semibold)
458 .fill(tokens::MUTED)
459 .radius(tokens::RADIUS_SM)
460 .width(Size::Fixed(30.0))
461 .height(Size::Fixed(30.0))
462}Sourcepub fn label(self) -> Self
pub fn label(self) -> Self
Examples found in repository?
337fn sale_row(
338 initials: &'static str,
339 name: &'static str,
340 email: &'static str,
341 amount: &'static str,
342) -> El {
343 row([
344 icon_cell(initials),
345 column([
346 text(name).semibold().ellipsis().width(Size::Fill(1.0)),
347 text(email).caption().ellipsis().width(Size::Fill(1.0)),
348 ])
349 .gap(2.0)
350 .height(Size::Hug)
351 .width(Size::Fill(1.0)),
352 text(amount).label().small(),
353 ])
354 .gap(tokens::SPACE_2)
355 .height(Size::Fixed(42.0))
356 .align(Align::Center)
357}
358
359fn documents_card() -> El {
360 card([
361 card_header([card_title("Documents")]).padding(tokens::SPACE_4),
362 card_content([scroll([table([
363 table_header([table_row([
364 table_head("").width(Size::Fixed(35.0)),
365 table_head("Header").width(Size::Fill(1.8)),
366 table_head("Section Type").width(Size::Fill(1.0)),
367 table_head("Status").width(Size::Fixed(104.0)),
368 table_head("Target").width(Size::Fixed(64.0)),
369 table_head("Limit").width(Size::Fixed(64.0)),
370 table_head("Reviewer").width(Size::Fixed(128.0)),
371 table_head("").width(Size::Fixed(32.0)),
372 ])
373 .padding(Sides::xy(tokens::SPACE_4, 0.0))
374 .key("metric:table.header")]),
375 divider(),
376 table_body([
377 document_row(
378 "Cover page",
379 "Cover page",
380 "In Process",
381 "18",
382 "5",
383 "Eddie Lake",
384 "info",
385 ),
386 document_row(
387 "Table of contents",
388 "Table of contents",
389 "Done",
390 "29",
391 "24",
392 "Eddie Lake",
393 "success",
394 ),
395 ]),
396 ])])
397 .height(Size::Fill(1.0))])
398 .gap(0.0)
399 .padding(0.0)
400 .height(Size::Fill(1.0)),
401 ])
402 .key("metric:table.card")
403 .height(Size::Fill(1.0))
404}
405
406fn document_row(
407 header: &'static str,
408 section: &'static str,
409 status: &'static str,
410 target: &'static str,
411 limit: &'static str,
412 reviewer: &'static str,
413 tone: &'static str,
414) -> El {
415 let status_badge = match tone {
416 "success" => badge(status).success(),
417 _ => badge(status).info(),
418 };
419 table_row([
420 table_utility_cell("::"),
421 table_cell(text(header).label().small()).width(Size::Fill(1.8)),
422 table_cell(text(section).muted()).width(Size::Fill(1.0)),
423 table_cell(status_badge).width(Size::Fixed(104.0)),
424 table_cell(text(target).label().small()).width(Size::Fixed(64.0)),
425 table_cell(text(limit).label().small()).width(Size::Fixed(64.0)),
426 table_cell(text(reviewer).muted()).width(Size::Fixed(128.0)),
427 table_action_cell(),
428 ])
429 .padding(Sides::xy(tokens::SPACE_4, 0.0))
430 .key(if header == "Cover page" {
431 "metric:table.row"
432 } else {
433 header
434 })
435}More examples
282fn token_chip(token: TokenDef, palette: &Palette) -> El {
283 let resolved = palette.resolve(token.color);
284 row([
285 El::new(Kind::Custom("palette-swatch"))
286 .at(file!(), line!())
287 .fill(token.color)
288 .stroke(tokens::BORDER)
289 .radius(tokens::RADIUS_SM)
290 .width(Size::Fixed(42.0))
291 .height(Size::Fixed(34.0)),
292 column([
293 text(token.name)
294 .label()
295 .ellipsis()
296 .nowrap_text()
297 .width(Size::Fill(1.0)),
298 mono(rgba_label(resolved)).caption().muted(),
299 ])
300 .gap(0.0)
301 .width(Size::Fill(1.0))
302 .height(Size::Hug),
303 ])
304 .gap(tokens::SPACE_2)
305 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_2))
306 .align(Align::Center)
307 .fill(tokens::CARD)
308 .stroke(tokens::BORDER)
309 .radius(tokens::RADIUS_MD)
310 .width(Size::Fill(1.0))
311 .height(Size::Fixed(54.0))
312}
313
314fn component_section() -> El {
315 card([
316 card_header([
317 card_title("Stock widgets"),
318 card_description("The same palette applied to regular component constructors."),
319 ]),
320 card_content([
321 row([
322 button("Primary").primary(),
323 button("Secondary").secondary(),
324 button("Outline").outline(),
325 button("Ghost").ghost(),
326 ])
327 .gap(tokens::SPACE_2)
328 .align(Align::Center),
329 row([
330 badge("success").success(),
331 badge("warning").warning(),
332 badge("destructive").destructive(),
333 badge("info").info(),
334 badge("muted").muted(),
335 ])
336 .gap(tokens::SPACE_2)
337 .align(Align::Center),
338 row([
339 text_input("palette search", &Selection::default(), "palette:search")
340 .width(Size::Fill(1.0)),
341 button_with_icon("settings", "Tune").secondary(),
342 ])
343 .gap(tokens::SPACE_2)
344 .align(Align::Center),
345 row([
346 surface_sample("Card", tokens::CARD),
347 surface_sample("Muted", tokens::MUTED),
348 surface_sample("Popover", tokens::POPOVER),
349 ])
350 .gap(tokens::SPACE_3)
351 .align(Align::Stretch),
352 ])
353 .gap(tokens::SPACE_4),
354 ])
355 .height(Size::Hug)
356}
357
358fn surface_sample(title: &'static str, fill: Color) -> El {
359 column([
360 text(title).label(),
361 text("surface sample").caption().muted(),
362 ])
363 .gap(tokens::SPACE_1)
364 .padding(tokens::SPACE_3)
365 .fill(fill)
366 .stroke(tokens::BORDER)
367 .radius(tokens::RADIUS_MD)
368 .width(Size::Fill(1.0))
369 .height(Size::Fixed(76.0))
370}237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}pub fn body(self) -> Self
pub fn title(self) -> Self
Sourcepub fn heading(self) -> Self
pub fn heading(self) -> Self
Examples found in repository?
164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}Sourcepub fn display(self) -> Self
pub fn display(self) -> Self
Examples found in repository?
143fn kpi_card(label: &'static str, value: &'static str, delta: &'static str, positive: bool) -> El {
144 let delta_badge = if positive {
145 badge(delta).success()
146 } else {
147 badge(delta).destructive()
148 };
149 let delta_badge = if label == "Latency" {
150 delta_badge.key("metric:kpi.badge")
151 } else {
152 delta_badge
153 };
154 let value_text = h2(value).display();
155 let value_text = if label == "Latency" {
156 value_text.key("metric:kpi.value")
157 } else {
158 value_text
159 };
160 card([
161 card_header([card_title(label)]),
162 card_content([
163 row([value_text, spacer(), delta_badge]).align(Align::Center),
164 text(if positive {
165 "Moving in the expected direction"
166 } else {
167 "Needs visual attention"
168 })
169 .muted(),
170 ])
171 .gap(tokens::SPACE_6),
172 ])
173 .key(if label == "Latency" {
174 "metric:kpi.card"
175 } else {
176 label
177 })
178 .width(Size::Fill(1.0))
179}Sourcepub fn bold(self) -> Self
pub fn bold(self) -> Self
Examples found in repository?
15fn scroll_list_fixture() -> El {
16 let rows: Vec<El> = (0..20)
17 .map(|i| {
18 row([
19 badge(format!("#{i}")).info(),
20 text(format!("Notification {i}")).bold(),
21 spacer(),
22 text(format!("{}m ago", i + 1)).muted(),
23 ])
24 .gap(tokens::SPACE_2)
25 .height(Size::Fixed(44.0))
26 .padding(Sides::xy(tokens::SPACE_3, tokens::SPACE_2))
27 })
28 .collect();
29
30 let list = scroll(rows)
31 .key("notifications")
32 .height(Size::Fixed(420.0))
33 .padding(tokens::SPACE_2);
34
35 column([
36 h2("Notifications"),
37 text("Roll the wheel inside the panel to scroll. The content is taller than the viewport.")
38 .muted(),
39 list,
40 ])
41 .gap(tokens::SPACE_4)
42 .padding(tokens::SPACE_7)
43}More examples
18fn fixture() -> El {
19 column([
20 h2("Inline runs"),
21 text_runs([
22 text("Aetna's attributed-text path lets you compose runs with "),
23 text("bold").bold(),
24 text(", "),
25 text("italic").italic(),
26 text(", "),
27 text("colored").color(tokens::DESTRUCTIVE),
28 text(", and "),
29 text("inline code").code(),
30 text(" segments inside one wrapping paragraph."),
31 hard_break(),
32 text("Hard breaks act like ").muted(),
33 text("<br>").code(),
34 text(" — they end the current line without breaking out of the run.").muted(),
35 ])
36 .wrap_text()
37 .width(Size::Fill(1.0))
38 .height(Size::Hug),
39 paragraph(
40 "All of the above flows through one cosmic-text rich-text shape — \
41 wrapping decisions cross run boundaries the way real prose wraps.",
42 )
43 .muted(),
44 ])
45 .gap(tokens::SPACE_4)
46 .padding(tokens::SPACE_7)
47 .width(Size::Fixed(640.0))
48}9fn modal_fixture() -> El {
10 stack([
11 column([
12 h1("Account"),
13 titled_card(
14 "Profile",
15 [
16 row([text("Email"), spacer(), text("user@example.com").muted()]),
17 row([text("Plan"), spacer(), badge("Pro").info()]),
18 ],
19 ),
20 titled_card(
21 "Danger zone",
22 [row([
23 column([
24 text("Delete account").bold(),
25 text("Remove this account and all associated data.")
26 .muted()
27 .small(),
28 ])
29 .gap(tokens::SPACE_1)
30 .align(Align::Start)
31 .width(Size::Hug),
32 spacer(),
33 button("Delete").destructive().key("open-delete"),
34 ])],
35 ),
36 ])
37 .gap(tokens::SPACE_4)
38 .padding(tokens::SPACE_7),
39 modal(
40 "delete-account",
41 "Delete account?",
42 [
43 text("Permanent action. Export data first.").muted(),
44 row([
45 spacer(),
46 button("Cancel").ghost().key("cancel-delete"),
47 button("Delete").destructive().key("confirm-delete"),
48 ])
49 .gap(tokens::SPACE_2),
50 ],
51 ),
52 ])
53}16fn settings() -> El {
17 column([
18 h1("Settings"),
19 titled_card(
20 "Account",
21 [
22 row([text("Email"), spacer(), text("user@example.com").muted()]),
23 row([
24 text("Two-factor authentication"),
25 spacer(),
26 badge("Enabled").success(),
27 ]),
28 row([
29 text("Recovery codes"),
30 spacer(),
31 button("Generate").secondary(),
32 ]),
33 ],
34 ),
35 titled_card(
36 "Appearance",
37 [
38 row([text("Theme"), spacer(), button("Dark").secondary()]),
39 row([text("Compact mode"), spacer(), badge("Off").muted()]),
40 row([text("Font size"), spacer(), text("14")]),
41 ],
42 ),
43 titled_card(
44 "Danger zone",
45 [row([
46 column([
47 text("Delete account").bold(),
48 text("Permanently remove your account and all data.")
49 .muted()
50 .small(),
51 ])
52 .gap(tokens::SPACE_1)
53 .align(Align::Start)
54 .width(Size::Hug),
55 spacer(),
56 button("Delete").destructive(),
57 ])],
58 ),
59 row([spacer(), button("Cancel").ghost(), button("Save").primary()]),
60 ])
61 .gap(tokens::SPACE_4)
62 .padding(tokens::SPACE_7)
63}26fn fixture() -> El {
27 column([
28 h2("Inline run backgrounds"),
29 paragraph(
30 "RunStyle.bg paints a per-line solid quad behind the glyphs of \
31 a styled span — the shaper computes the rect from the actual \
32 glyph extents, so wrapping splits the highlight cleanly.",
33 )
34 .muted(),
35 // Search-result style.
36 text_runs([
37 text("…the matcher finds "),
38 text("aetna").background(HIGHLIGHT_YELLOW).bold(),
39 text(" in "),
40 text("aetna_core::widgets").mono(),
41 text(" — the highlight tracks the glyph extent."),
42 ])
43 .wrap_text()
44 .width(Size::Fill(1.0))
45 .height(Size::Hug),
46 // Diff-style: add + remove tints inside the same line.
47 text_runs([
48 text("- "),
49 text("error::Custom").mono().background(DIFF_REMOVE),
50 text("(\"too narrow\")"),
51 hard_break(),
52 text("+ "),
53 text("error::WrapTooNarrow").mono().background(DIFF_ADD),
54 text(" { available }"),
55 ])
56 .wrap_text()
57 .width(Size::Fill(1.0))
58 .height(Size::Hug),
59 // Wrapping highlight: long span that spans two lines.
60 text_runs([
61 text("Long highlight: "),
62 text("the quick brown fox jumps over the lazy dog and keeps going")
63 .background(HIGHLIGHT_YELLOW),
64 text(" — the rect is split per line."),
65 ])
66 .wrap_text()
67 .width(Size::Fill(1.0))
68 .height(Size::Hug),
69 ])
70 .gap(tokens::SPACE_4)
71 .padding(tokens::SPACE_7)
72 .width(Size::Fixed(640.0))
73}15fn fixture() -> El {
16 column([
17 h2("Long-form content widgets"),
18 paragraph(
19 "These primitives compose the markdown-shaped vocabulary an \
20 upcoming transformer will target. Each widget is plain \
21 Aetna — selectable text, themed surfaces, the same layout \
22 pass as everything else.",
23 ),
24 h3("Highlights"),
25 bullet_list(vec![
26 text_runs([
27 text("Bulleted lists with a hanging indent — wrapped lines align under "),
28 text("themselves").italic(),
29 text(", not under the marker."),
30 ]),
31 text_runs([
32 text("Inline runs work inside list items: "),
33 text("bold").bold(),
34 text(", "),
35 text("code").code(),
36 text(", "),
37 text("links").link("https://aetna.dev"),
38 text("."),
39 ]),
40 text("Nested blocks live inside an item by composing a column."),
41 ]),
42 blockquote([
43 paragraph(
44 "Markdown's shape is HTML's shape. The Aetna widget kit \
45 already mirrors most of that shape, so the transformer \
46 mostly hands events to existing constructors.",
47 ),
48 paragraph("— Aetna design notes").muted(),
49 ]),
50 h3("Setup steps"),
51 numbered_list(vec![
52 text("Add `aetna-markdown` to the workspace."),
53 text_runs([
54 text("Pull in "),
55 text("pulldown-cmark").code(),
56 text(" with the GFM features the project actually uses."),
57 ]),
58 text("Wire the transformer through the existing widget kit — paragraph, list, blockquote, code_block, divider, table."),
59 ]),
60 h3("Example fenced block"),
61 code_block(
62 "fn render(md: &str) -> El {\n \
63 // pulldown-cmark events -> El\n \
64 todo!(\"phase 2\")\n}",
65 ),
66 ])
67 .gap(tokens::SPACE_4)
68 .padding(tokens::SPACE_7)
69 .width(Size::Fixed(640.0))
70}Sourcepub fn semibold(self) -> Self
pub fn semibold(self) -> Self
Examples found in repository?
41fn settings_sidebar() -> El {
42 column([
43 row([
44 icon_slot("settings"),
45 column([
46 text("Workspace")
47 .semibold()
48 .ellipsis()
49 .width(Size::Fill(1.0)),
50 text("Settings").caption().ellipsis().width(Size::Fill(1.0)),
51 ])
52 .gap(2.0)
53 .width(Size::Fill(1.0))
54 .height(Size::Hug),
55 ])
56 .gap(tokens::SPACE_2)
57 .height(Size::Fixed(44.0))
58 .align(Align::Center),
59 section_label("Personal"),
60 side_item("users", "Profile", false),
61 side_item("settings", "Account", true),
62 side_item("alert-circle", "Security", false),
63 side_item("bell", "Notifications", false),
64 spacer().height(Size::Fixed(tokens::SPACE_4)),
65 section_label("Workspace"),
66 side_item("file-text", "Billing", false),
67 side_item("bar-chart", "Appearance", false),
68 side_item("activity", "Integrations", false),
69 spacer(),
70 column([text("Changes sync after save.").caption().wrap_text()])
71 .padding(tokens::SPACE_2)
72 .fill(tokens::MUTED)
73 .radius(tokens::RADIUS_MD),
74 ])
75 .gap(tokens::SPACE_2)
76 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
77 .key("metric:sidebar")
78 .width(Size::Fixed(244.0))
79 .height(Size::Fill(1.0))
80 .fill(tokens::CARD)
81 .stroke(tokens::BORDER)
82}
83
84fn settings_main() -> El {
85 column([
86 settings_header(),
87 row([settings_nav_card(), settings_body(), settings_aside()])
88 .gap(tokens::SPACE_4)
89 .padding(tokens::SPACE_4)
90 .height(Size::Fill(1.0))
91 .align(Align::Stretch),
92 ])
93 .width(Size::Fill(1.0))
94 .height(Size::Fill(1.0))
95}
96
97fn settings_header() -> El {
98 row([
99 icon_button("menu").ghost(),
100 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
101 h3("Settings").key("metric:page.title"),
102 spacer(),
103 button("Reset").secondary(),
104 button("Save changes").primary(),
105 ])
106 .key("metric:header")
107 .gap(tokens::SPACE_3)
108 .height(Size::Fixed(56.0))
109 .padding(Sides::xy(tokens::SPACE_4, 0.0))
110 .align(Align::Center)
111 .stroke(tokens::BORDER)
112}
113
114fn settings_nav_card() -> El {
115 column([
116 settings_nav_item("Account", true),
117 settings_nav_item("Security", false),
118 settings_nav_item("Notifications", false),
119 settings_nav_item("Appearance", false),
120 settings_nav_item("Billing", false),
121 ])
122 .gap(tokens::SPACE_1)
123 .padding(tokens::SPACE_1)
124 .width(Size::Fixed(220.0))
125 .height(Size::Fill(1.0))
126 .style_profile(StyleProfile::Surface)
127 .surface_role(SurfaceRole::Panel)
128 .fill(tokens::CARD)
129 .stroke(tokens::BORDER)
130 .radius(tokens::RADIUS_MD)
131 .shadow(tokens::SHADOW_MD)
132}
133
134fn settings_nav_item(label: &'static str, selected: bool) -> El {
135 let mut item = row([
136 El::new(Kind::Custom("nav-dot"))
137 .fill(tokens::MUTED_FOREGROUND)
138 .radius(tokens::RADIUS_PILL)
139 .width(Size::Fixed(6.0))
140 .height(Size::Fixed(6.0)),
141 text(label)
142 .font_weight(FontWeight::Medium)
143 .ellipsis()
144 .width(Size::Fill(1.0)),
145 ])
146 .key(if selected {
147 "metric:settings.nav.row".to_string()
148 } else {
149 format!("settings-nav-{label}")
150 })
151 .metrics_role(MetricsRole::ListItem)
152 .align(Align::Center)
153 .focusable();
154
155 if selected {
156 item = item.current();
157 } else {
158 item = item.color(tokens::MUTED_FOREGROUND);
159 }
160
161 item
162}
163
164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}
185
186fn profile_card() -> El {
187 card([
188 card_header([
189 card_title("Profile"),
190 card_description("This information appears in audit logs and shared documents."),
191 ]),
192 card_content([form([
193 row([
194 setting_field("Display name", "Alicia Koch", "display-name"),
195 setting_field("Email", "alicia@acme.co", "email"),
196 ])
197 .gap(tokens::SPACE_3),
198 row([
199 setting_select("Role", "Workspace admin", "role"),
200 setting_select("Region", "US East", "region"),
201 ])
202 .gap(tokens::SPACE_3),
203 ])]),
204 ])
205 .key("metric:profile.card")
206}
207
208fn setting_field(label: &'static str, value: &'static str, key: &'static str) -> El {
209 form_item([
210 form_label(label),
211 form_control(
212 text_input(value, &Selection::caret(key, value.len()), key).key(
213 if key == "display-name" {
214 "metric:form.input"
215 } else {
216 key
217 },
218 ),
219 ),
220 ])
221 .width(Size::Fill(1.0))
222}
223
224fn setting_select(label: &'static str, value: &'static str, key: &'static str) -> El {
225 form_item([form_label(label), form_control(select_trigger(key, value))]).width(Size::Fill(1.0))
226}
227
228fn preferences_card() -> El {
229 card([
230 card_header([
231 card_title("Preferences"),
232 card_description("Defaults used when creating new dashboards and exports."),
233 ]),
234 card_content([column([
235 preference_row(
236 "Compact navigation",
237 "Use tighter rows in the sidebar and command menus.",
238 switch(true).key("compact-navigation"),
239 ),
240 divider(),
241 preference_row(
242 "Email summaries",
243 "Send a daily digest when documents change.",
244 switch(false).key("email-summaries"),
245 ),
246 divider(),
247 preference_row(
248 "Require approval",
249 "Route external sharing through an owner review.",
250 checkbox(true).key("approval-required"),
251 ),
252 ])
253 .gap(0.0)
254 .width(Size::Fill(1.0))])
255 .padding(0.0),
256 ])
257 .key("metric:preferences.card")
258}
259
260fn preference_row(title: &'static str, description: &'static str, control: El) -> El {
261 row([
262 column([
263 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
264 text(description)
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0))
271 .height(Size::Hug),
272 control,
273 ])
274 .key(if title == "Compact navigation" {
275 "metric:preference.row".to_string()
276 } else {
277 format!("preference-{title}")
278 })
279 .metrics_role(MetricsRole::PreferenceRow)
280 .gap(tokens::SPACE_4)
281 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_3))
282 .align(Align::Center)
283}
284
285fn settings_aside() -> El {
286 column([security_card(), scale_card()])
287 .gap(tokens::SPACE_4)
288 .width(Size::Fixed(300.0))
289 .height(Size::Fill(1.0))
290}
291
292fn security_card() -> El {
293 card([
294 card_header([
295 card_title("Security"),
296 card_description("Two-factor authentication is enabled for all privileged users."),
297 ]),
298 card_content([
299 compact_stat("Passkeys", "2 registered", badge("On").success()),
300 compact_stat("Sessions", "3 active", button("Review").secondary()),
301 ]),
302 ])
303 .width(Size::Fill(1.0))
304}
305
306fn scale_card() -> El {
307 card([
308 card_header([
309 card_title("Interface scale"),
310 card_description("Reference captures keep browser zoom fixed and vary root UI scale."),
311 ]),
312 card_content([
313 row([text("Dense").caption(), spacer(), text("Default").caption()]),
314 slider(0.66, tokens::PRIMARY)
315 .key("interface-scale")
316 .width(Size::Fill(1.0)),
317 ]),
318 ])
319 .width(Size::Fill(1.0))
320}
321
322fn compact_stat(title: &'static str, detail: &'static str, control: El) -> El {
323 row([
324 column([
325 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
326 text(detail).caption().ellipsis().width(Size::Fill(1.0)),
327 ])
328 .gap(2.0)
329 .width(Size::Fill(1.0))
330 .height(Size::Hug),
331 control,
332 ])
333 .gap(tokens::SPACE_2)
334 .height(Size::Fixed(44.0))
335 .align(Align::Center)
336}More examples
41fn dashboard_sidebar() -> El {
42 column([
43 row([
44 icon_cell("A"),
45 column([
46 text("Acme Inc.")
47 .semibold()
48 .ellipsis()
49 .width(Size::Fill(1.0)),
50 text("Enterprise")
51 .caption()
52 .ellipsis()
53 .width(Size::Fill(1.0)),
54 ])
55 .gap(2.0)
56 .width(Size::Fill(1.0))
57 .height(Size::Hug),
58 ])
59 .gap(tokens::SPACE_2)
60 .height(Size::Fixed(44.0))
61 .align(Align::Center),
62 section_label("Platform"),
63 side_item("layout-dashboard", "Dashboard", true),
64 side_item("activity", "Lifecycle", false),
65 side_item("bar-chart", "Analytics", false),
66 side_item("folder", "Projects", false),
67 spacer().height(Size::Fixed(tokens::SPACE_4)),
68 section_label("Documents"),
69 side_item("file-text", "Data library", false),
70 side_item("download", "Reports", false),
71 side_item("users", "Team", false),
72 spacer(),
73 row([
74 icon_cell("AK"),
75 column([
76 text("Alicia Koch")
77 .semibold()
78 .ellipsis()
79 .width(Size::Fill(1.0)),
80 text("alicia@example.com")
81 .caption()
82 .ellipsis()
83 .width(Size::Fill(1.0)),
84 ])
85 .gap(2.0)
86 .width(Size::Fill(1.0))
87 .height(Size::Hug),
88 ])
89 .gap(tokens::SPACE_2)
90 .height(Size::Fixed(50.0))
91 .align(Align::Center),
92 ])
93 .gap(tokens::SPACE_2)
94 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
95 .key("metric:sidebar")
96 .width(Size::Fixed(244.0))
97 .height(Size::Fill(1.0))
98 .fill(tokens::CARD)
99 .stroke(tokens::BORDER)
100}
101
102fn section_label(label: &'static str) -> El {
103 text(label)
104 .caption()
105 .height(Size::Fixed(22.0))
106 .padding(Sides::xy(tokens::SPACE_2, 0.0))
107}
108
109fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
110 let mut item = row([
111 icon(icon_name)
112 .color(tokens::MUTED_FOREGROUND)
113 .icon_size(tokens::ICON_SM)
114 .width(Size::Fixed(tokens::ICON_SM)),
115 text(label)
116 .font_weight(FontWeight::Medium)
117 .ellipsis()
118 .width(Size::Fill(1.0)),
119 ])
120 .key(if selected {
121 "metric:sidebar.nav.row".to_string()
122 } else {
123 format!("side-item-{label}")
124 })
125 .metrics_role(MetricsRole::ListItem)
126 .gap(tokens::SPACE_2)
127 .padding(Sides::xy(tokens::SPACE_2, 0.0))
128 .height(Size::Fixed(32.0))
129 .align(Align::Center)
130 .focusable();
131
132 if selected {
133 item = item.current();
134 } else {
135 item = item.color(tokens::MUTED_FOREGROUND);
136 }
137
138 item
139}
140
141fn dashboard_main() -> El {
142 column([
143 dashboard_header(),
144 column([
145 row([
146 metric_card(
147 "bar-chart",
148 "Total Revenue",
149 "$1,250.00",
150 "+12.5%",
151 "Trending up this month",
152 true,
153 ),
154 metric_card(
155 "users",
156 "New Customers",
157 "1,234",
158 "-20%",
159 "Acquisition needs attention",
160 false,
161 ),
162 metric_card(
163 "folder",
164 "Active Accounts",
165 "45,678",
166 "+12.5%",
167 "Strong user retention",
168 true,
169 ),
170 metric_card(
171 "activity",
172 "Growth Rate",
173 "4.5%",
174 "+4.5%",
175 "Meets growth projections",
176 true,
177 ),
178 ])
179 .gap(tokens::SPACE_4),
180 row([chart_card(), sales_card()])
181 .gap(tokens::SPACE_4)
182 .height(Size::Fixed(306.0))
183 .align(Align::Stretch),
184 documents_card(),
185 ])
186 .gap(tokens::SPACE_4)
187 .padding(tokens::SPACE_7)
188 .height(Size::Fill(1.0)),
189 ])
190 .width(Size::Fill(1.0))
191 .height(Size::Fill(1.0))
192}
193
194fn dashboard_header() -> El {
195 row([
196 icon_button("menu").ghost(),
197 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
198 h3("Documents").key("metric:page.title"),
199 spacer(),
200 text_input("Search...", &Selection::default(), "dashboard-search")
201 .key("metric:command.input")
202 .width(Size::Fixed(260.0)),
203 icon_button("plus").ghost(),
204 icon_button("bell").ghost(),
205 ])
206 .key("metric:header")
207 .gap(tokens::SPACE_3)
208 .height(Size::Fixed(56.0))
209 .padding(Sides::xy(tokens::SPACE_4, 0.0))
210 .align(Align::Center)
211 .stroke(tokens::BORDER)
212}
213
214fn metric_card(
215 icon_name: &'static str,
216 title: &'static str,
217 value: &'static str,
218 delta: &'static str,
219 note: &'static str,
220 positive: bool,
221) -> El {
222 let badge = if positive {
223 badge(delta).success()
224 } else {
225 badge(delta).warning()
226 };
227 let badge = if title == "Total Revenue" {
228 badge.key("metric:kpi.badge")
229 } else {
230 badge
231 };
232 let value = if title == "Total Revenue" {
233 h2(value).ellipsis().key("metric:kpi.value")
234 } else {
235 h2(value).ellipsis()
236 };
237 card([card_content([
238 row([
239 row([
240 icon(icon_name)
241 .color(tokens::MUTED_FOREGROUND)
242 .icon_size(tokens::ICON_XS),
243 text(title).muted().ellipsis().width(Size::Fill(1.0)),
244 ])
245 .gap(tokens::SPACE_1)
246 .width(Size::Fill(1.0))
247 .align(Align::Center),
248 badge,
249 ])
250 .gap(tokens::SPACE_2)
251 .align(Align::Center),
252 value,
253 text(note).caption().ellipsis().width(Size::Fill(1.0)),
254 ])
255 .padding(tokens::SPACE_4)
256 .gap(tokens::SPACE_2)])
257 .key(if title == "Total Revenue" {
258 "metric:kpi.card"
259 } else {
260 title
261 })
262 .width(Size::Fill(1.0))
263}
264
265fn chart_card() -> El {
266 card([
267 card_header([
268 card_title("Visitors for the last 6 months"),
269 card_description("Total visitors by channel."),
270 ])
271 .padding(tokens::SPACE_4),
272 card_content([row(chart_bars())
273 .gap(2.0)
274 .height(Size::Fill(1.0))
275 .align(Align::End)])
276 .padding(Sides {
277 left: tokens::SPACE_4,
278 right: tokens::SPACE_4,
279 top: 0.0,
280 bottom: tokens::SPACE_4,
281 })
282 .height(Size::Fill(1.0)),
283 ])
284 .key("metric:chart.card")
285 .width(Size::Fill(1.0))
286 .height(Size::Fill(1.0))
287}
288
289fn chart_bars() -> Vec<El> {
290 [
291 48.0, 72.0, 56.0, 90.0, 64.0, 80.0, 108.0, 84.0, 122.0, 96.0, 136.0, 118.0,
292 ]
293 .into_iter()
294 .flat_map(|height| {
295 [
296 bar(height, tokens::MUTED_FOREGROUND),
297 bar((height - 28.0_f32).max(24.0), tokens::INPUT),
298 ]
299 })
300 .collect()
301}
302
303fn bar(height: f32, color: Color) -> El {
304 El::new(Kind::Custom("chart_bar"))
305 .fill(color)
306 .radius(tokens::RADIUS_SM)
307 .width(Size::Fill(1.0))
308 .height(Size::Fixed(height))
309}
310
311fn sales_card() -> El {
312 card([
313 card_header([
314 card_title("Recent Sales"),
315 card_description("You made 265 sales this month."),
316 ])
317 .padding(tokens::SPACE_4),
318 card_content([
319 sale_row("OM", "Olivia Martin", "olivia@example.com", "+$1,999.00"),
320 sale_row("JL", "Jackson Lee", "jackson@example.com", "+$39.00"),
321 sale_row("IN", "Isabella Nguyen", "isabella@example.com", "+$299.00"),
322 sale_row("WK", "William Kim", "will@example.com", "+$99.00"),
323 ])
324 .gap(tokens::SPACE_2)
325 .padding(Sides {
326 left: tokens::SPACE_4,
327 right: tokens::SPACE_4,
328 top: 0.0,
329 bottom: tokens::SPACE_4,
330 }),
331 ])
332 .key("metric:sales.card")
333 .width(Size::Fixed(330.0))
334 .height(Size::Fill(1.0))
335}
336
337fn sale_row(
338 initials: &'static str,
339 name: &'static str,
340 email: &'static str,
341 amount: &'static str,
342) -> El {
343 row([
344 icon_cell(initials),
345 column([
346 text(name).semibold().ellipsis().width(Size::Fill(1.0)),
347 text(email).caption().ellipsis().width(Size::Fill(1.0)),
348 ])
349 .gap(2.0)
350 .height(Size::Hug)
351 .width(Size::Fill(1.0)),
352 text(amount).label().small(),
353 ])
354 .gap(tokens::SPACE_2)
355 .height(Size::Fixed(42.0))
356 .align(Align::Center)
357}Sourcepub fn small(self) -> Self
pub fn small(self) -> Self
Examples found in repository?
337fn sale_row(
338 initials: &'static str,
339 name: &'static str,
340 email: &'static str,
341 amount: &'static str,
342) -> El {
343 row([
344 icon_cell(initials),
345 column([
346 text(name).semibold().ellipsis().width(Size::Fill(1.0)),
347 text(email).caption().ellipsis().width(Size::Fill(1.0)),
348 ])
349 .gap(2.0)
350 .height(Size::Hug)
351 .width(Size::Fill(1.0)),
352 text(amount).label().small(),
353 ])
354 .gap(tokens::SPACE_2)
355 .height(Size::Fixed(42.0))
356 .align(Align::Center)
357}
358
359fn documents_card() -> El {
360 card([
361 card_header([card_title("Documents")]).padding(tokens::SPACE_4),
362 card_content([scroll([table([
363 table_header([table_row([
364 table_head("").width(Size::Fixed(35.0)),
365 table_head("Header").width(Size::Fill(1.8)),
366 table_head("Section Type").width(Size::Fill(1.0)),
367 table_head("Status").width(Size::Fixed(104.0)),
368 table_head("Target").width(Size::Fixed(64.0)),
369 table_head("Limit").width(Size::Fixed(64.0)),
370 table_head("Reviewer").width(Size::Fixed(128.0)),
371 table_head("").width(Size::Fixed(32.0)),
372 ])
373 .padding(Sides::xy(tokens::SPACE_4, 0.0))
374 .key("metric:table.header")]),
375 divider(),
376 table_body([
377 document_row(
378 "Cover page",
379 "Cover page",
380 "In Process",
381 "18",
382 "5",
383 "Eddie Lake",
384 "info",
385 ),
386 document_row(
387 "Table of contents",
388 "Table of contents",
389 "Done",
390 "29",
391 "24",
392 "Eddie Lake",
393 "success",
394 ),
395 ]),
396 ])])
397 .height(Size::Fill(1.0))])
398 .gap(0.0)
399 .padding(0.0)
400 .height(Size::Fill(1.0)),
401 ])
402 .key("metric:table.card")
403 .height(Size::Fill(1.0))
404}
405
406fn document_row(
407 header: &'static str,
408 section: &'static str,
409 status: &'static str,
410 target: &'static str,
411 limit: &'static str,
412 reviewer: &'static str,
413 tone: &'static str,
414) -> El {
415 let status_badge = match tone {
416 "success" => badge(status).success(),
417 _ => badge(status).info(),
418 };
419 table_row([
420 table_utility_cell("::"),
421 table_cell(text(header).label().small()).width(Size::Fill(1.8)),
422 table_cell(text(section).muted()).width(Size::Fill(1.0)),
423 table_cell(status_badge).width(Size::Fixed(104.0)),
424 table_cell(text(target).label().small()).width(Size::Fixed(64.0)),
425 table_cell(text(limit).label().small()).width(Size::Fixed(64.0)),
426 table_cell(text(reviewer).muted()).width(Size::Fixed(128.0)),
427 table_action_cell(),
428 ])
429 .padding(Sides::xy(tokens::SPACE_4, 0.0))
430 .key(if header == "Cover page" {
431 "metric:table.row"
432 } else {
433 header
434 })
435}More examples
237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}9fn modal_fixture() -> El {
10 stack([
11 column([
12 h1("Account"),
13 titled_card(
14 "Profile",
15 [
16 row([text("Email"), spacer(), text("user@example.com").muted()]),
17 row([text("Plan"), spacer(), badge("Pro").info()]),
18 ],
19 ),
20 titled_card(
21 "Danger zone",
22 [row([
23 column([
24 text("Delete account").bold(),
25 text("Remove this account and all associated data.")
26 .muted()
27 .small(),
28 ])
29 .gap(tokens::SPACE_1)
30 .align(Align::Start)
31 .width(Size::Hug),
32 spacer(),
33 button("Delete").destructive().key("open-delete"),
34 ])],
35 ),
36 ])
37 .gap(tokens::SPACE_4)
38 .padding(tokens::SPACE_7),
39 modal(
40 "delete-account",
41 "Delete account?",
42 [
43 text("Permanent action. Export data first.").muted(),
44 row([
45 spacer(),
46 button("Cancel").ghost().key("cancel-delete"),
47 button("Delete").destructive().key("confirm-delete"),
48 ])
49 .gap(tokens::SPACE_2),
50 ],
51 ),
52 ])
53}16fn settings() -> El {
17 column([
18 h1("Settings"),
19 titled_card(
20 "Account",
21 [
22 row([text("Email"), spacer(), text("user@example.com").muted()]),
23 row([
24 text("Two-factor authentication"),
25 spacer(),
26 badge("Enabled").success(),
27 ]),
28 row([
29 text("Recovery codes"),
30 spacer(),
31 button("Generate").secondary(),
32 ]),
33 ],
34 ),
35 titled_card(
36 "Appearance",
37 [
38 row([text("Theme"), spacer(), button("Dark").secondary()]),
39 row([text("Compact mode"), spacer(), badge("Off").muted()]),
40 row([text("Font size"), spacer(), text("14")]),
41 ],
42 ),
43 titled_card(
44 "Danger zone",
45 [row([
46 column([
47 text("Delete account").bold(),
48 text("Permanently remove your account and all data.")
49 .muted()
50 .small(),
51 ])
52 .gap(tokens::SPACE_1)
53 .align(Align::Start)
54 .width(Size::Hug),
55 spacer(),
56 button("Delete").destructive(),
57 ])],
58 ),
59 row([spacer(), button("Cancel").ghost(), button("Save").primary()]),
60 ])
61 .gap(tokens::SPACE_4)
62 .padding(tokens::SPACE_7)
63}pub fn xsmall(self) -> Self
Sourcepub fn color(self, c: Color) -> Self
pub fn color(self, c: Color) -> Self
Set an explicit text color.
Examples found in repository?
134fn settings_nav_item(label: &'static str, selected: bool) -> El {
135 let mut item = row([
136 El::new(Kind::Custom("nav-dot"))
137 .fill(tokens::MUTED_FOREGROUND)
138 .radius(tokens::RADIUS_PILL)
139 .width(Size::Fixed(6.0))
140 .height(Size::Fixed(6.0)),
141 text(label)
142 .font_weight(FontWeight::Medium)
143 .ellipsis()
144 .width(Size::Fill(1.0)),
145 ])
146 .key(if selected {
147 "metric:settings.nav.row".to_string()
148 } else {
149 format!("settings-nav-{label}")
150 })
151 .metrics_role(MetricsRole::ListItem)
152 .align(Align::Center)
153 .focusable();
154
155 if selected {
156 item = item.current();
157 } else {
158 item = item.color(tokens::MUTED_FOREGROUND);
159 }
160
161 item
162}
163
164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}
185
186fn profile_card() -> El {
187 card([
188 card_header([
189 card_title("Profile"),
190 card_description("This information appears in audit logs and shared documents."),
191 ]),
192 card_content([form([
193 row([
194 setting_field("Display name", "Alicia Koch", "display-name"),
195 setting_field("Email", "alicia@acme.co", "email"),
196 ])
197 .gap(tokens::SPACE_3),
198 row([
199 setting_select("Role", "Workspace admin", "role"),
200 setting_select("Region", "US East", "region"),
201 ])
202 .gap(tokens::SPACE_3),
203 ])]),
204 ])
205 .key("metric:profile.card")
206}
207
208fn setting_field(label: &'static str, value: &'static str, key: &'static str) -> El {
209 form_item([
210 form_label(label),
211 form_control(
212 text_input(value, &Selection::caret(key, value.len()), key).key(
213 if key == "display-name" {
214 "metric:form.input"
215 } else {
216 key
217 },
218 ),
219 ),
220 ])
221 .width(Size::Fill(1.0))
222}
223
224fn setting_select(label: &'static str, value: &'static str, key: &'static str) -> El {
225 form_item([form_label(label), form_control(select_trigger(key, value))]).width(Size::Fill(1.0))
226}
227
228fn preferences_card() -> El {
229 card([
230 card_header([
231 card_title("Preferences"),
232 card_description("Defaults used when creating new dashboards and exports."),
233 ]),
234 card_content([column([
235 preference_row(
236 "Compact navigation",
237 "Use tighter rows in the sidebar and command menus.",
238 switch(true).key("compact-navigation"),
239 ),
240 divider(),
241 preference_row(
242 "Email summaries",
243 "Send a daily digest when documents change.",
244 switch(false).key("email-summaries"),
245 ),
246 divider(),
247 preference_row(
248 "Require approval",
249 "Route external sharing through an owner review.",
250 checkbox(true).key("approval-required"),
251 ),
252 ])
253 .gap(0.0)
254 .width(Size::Fill(1.0))])
255 .padding(0.0),
256 ])
257 .key("metric:preferences.card")
258}
259
260fn preference_row(title: &'static str, description: &'static str, control: El) -> El {
261 row([
262 column([
263 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
264 text(description)
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0))
271 .height(Size::Hug),
272 control,
273 ])
274 .key(if title == "Compact navigation" {
275 "metric:preference.row".to_string()
276 } else {
277 format!("preference-{title}")
278 })
279 .metrics_role(MetricsRole::PreferenceRow)
280 .gap(tokens::SPACE_4)
281 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_3))
282 .align(Align::Center)
283}
284
285fn settings_aside() -> El {
286 column([security_card(), scale_card()])
287 .gap(tokens::SPACE_4)
288 .width(Size::Fixed(300.0))
289 .height(Size::Fill(1.0))
290}
291
292fn security_card() -> El {
293 card([
294 card_header([
295 card_title("Security"),
296 card_description("Two-factor authentication is enabled for all privileged users."),
297 ]),
298 card_content([
299 compact_stat("Passkeys", "2 registered", badge("On").success()),
300 compact_stat("Sessions", "3 active", button("Review").secondary()),
301 ]),
302 ])
303 .width(Size::Fill(1.0))
304}
305
306fn scale_card() -> El {
307 card([
308 card_header([
309 card_title("Interface scale"),
310 card_description("Reference captures keep browser zoom fixed and vary root UI scale."),
311 ]),
312 card_content([
313 row([text("Dense").caption(), spacer(), text("Default").caption()]),
314 slider(0.66, tokens::PRIMARY)
315 .key("interface-scale")
316 .width(Size::Fill(1.0)),
317 ]),
318 ])
319 .width(Size::Fill(1.0))
320}
321
322fn compact_stat(title: &'static str, detail: &'static str, control: El) -> El {
323 row([
324 column([
325 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
326 text(detail).caption().ellipsis().width(Size::Fill(1.0)),
327 ])
328 .gap(2.0)
329 .width(Size::Fill(1.0))
330 .height(Size::Hug),
331 control,
332 ])
333 .gap(tokens::SPACE_2)
334 .height(Size::Fixed(44.0))
335 .align(Align::Center)
336}
337
338fn section_label(label: &'static str) -> El {
339 text(label)
340 .caption()
341 .height(Size::Fixed(22.0))
342 .padding(Sides::xy(tokens::SPACE_2, 0.0))
343}
344
345fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
346 let mut item = row([
347 icon(icon_name)
348 .color(tokens::MUTED_FOREGROUND)
349 .icon_size(tokens::ICON_SM)
350 .width(Size::Fixed(tokens::ICON_SM)),
351 text(label)
352 .font_weight(FontWeight::Medium)
353 .ellipsis()
354 .width(Size::Fill(1.0)),
355 ])
356 .key(if selected {
357 "metric:sidebar.nav.row".to_string()
358 } else {
359 format!("side-item-{label}")
360 })
361 .metrics_role(MetricsRole::ListItem)
362 .gap(tokens::SPACE_2)
363 .padding(Sides::xy(tokens::SPACE_2, 0.0))
364 .height(Size::Fixed(32.0))
365 .align(Align::Center)
366 .focusable();
367
368 if selected {
369 item = item.current();
370 } else {
371 item = item.color(tokens::MUTED_FOREGROUND);
372 }
373
374 item
375}
376
377fn icon_slot(icon_name: &'static str) -> El {
378 El::new(Kind::Custom("icon_cell"))
379 .style_profile(StyleProfile::Surface)
380 .child(
381 icon(icon_name)
382 .color(tokens::FOREGROUND)
383 .icon_size(tokens::ICON_XS),
384 )
385 .align(Align::Center)
386 .justify(Justify::Center)
387 .fill(tokens::MUTED)
388 .stroke(tokens::BORDER)
389 .radius(tokens::RADIUS_SM)
390 .width(Size::Fixed(30.0))
391 .height(Size::Fixed(30.0))
392}More examples
109fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
110 let mut item = row([
111 icon(icon_name)
112 .color(tokens::MUTED_FOREGROUND)
113 .icon_size(tokens::ICON_SM)
114 .width(Size::Fixed(tokens::ICON_SM)),
115 text(label)
116 .font_weight(FontWeight::Medium)
117 .ellipsis()
118 .width(Size::Fill(1.0)),
119 ])
120 .key(if selected {
121 "metric:sidebar.nav.row".to_string()
122 } else {
123 format!("side-item-{label}")
124 })
125 .metrics_role(MetricsRole::ListItem)
126 .gap(tokens::SPACE_2)
127 .padding(Sides::xy(tokens::SPACE_2, 0.0))
128 .height(Size::Fixed(32.0))
129 .align(Align::Center)
130 .focusable();
131
132 if selected {
133 item = item.current();
134 } else {
135 item = item.color(tokens::MUTED_FOREGROUND);
136 }
137
138 item
139}
140
141fn dashboard_main() -> El {
142 column([
143 dashboard_header(),
144 column([
145 row([
146 metric_card(
147 "bar-chart",
148 "Total Revenue",
149 "$1,250.00",
150 "+12.5%",
151 "Trending up this month",
152 true,
153 ),
154 metric_card(
155 "users",
156 "New Customers",
157 "1,234",
158 "-20%",
159 "Acquisition needs attention",
160 false,
161 ),
162 metric_card(
163 "folder",
164 "Active Accounts",
165 "45,678",
166 "+12.5%",
167 "Strong user retention",
168 true,
169 ),
170 metric_card(
171 "activity",
172 "Growth Rate",
173 "4.5%",
174 "+4.5%",
175 "Meets growth projections",
176 true,
177 ),
178 ])
179 .gap(tokens::SPACE_4),
180 row([chart_card(), sales_card()])
181 .gap(tokens::SPACE_4)
182 .height(Size::Fixed(306.0))
183 .align(Align::Stretch),
184 documents_card(),
185 ])
186 .gap(tokens::SPACE_4)
187 .padding(tokens::SPACE_7)
188 .height(Size::Fill(1.0)),
189 ])
190 .width(Size::Fill(1.0))
191 .height(Size::Fill(1.0))
192}
193
194fn dashboard_header() -> El {
195 row([
196 icon_button("menu").ghost(),
197 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
198 h3("Documents").key("metric:page.title"),
199 spacer(),
200 text_input("Search...", &Selection::default(), "dashboard-search")
201 .key("metric:command.input")
202 .width(Size::Fixed(260.0)),
203 icon_button("plus").ghost(),
204 icon_button("bell").ghost(),
205 ])
206 .key("metric:header")
207 .gap(tokens::SPACE_3)
208 .height(Size::Fixed(56.0))
209 .padding(Sides::xy(tokens::SPACE_4, 0.0))
210 .align(Align::Center)
211 .stroke(tokens::BORDER)
212}
213
214fn metric_card(
215 icon_name: &'static str,
216 title: &'static str,
217 value: &'static str,
218 delta: &'static str,
219 note: &'static str,
220 positive: bool,
221) -> El {
222 let badge = if positive {
223 badge(delta).success()
224 } else {
225 badge(delta).warning()
226 };
227 let badge = if title == "Total Revenue" {
228 badge.key("metric:kpi.badge")
229 } else {
230 badge
231 };
232 let value = if title == "Total Revenue" {
233 h2(value).ellipsis().key("metric:kpi.value")
234 } else {
235 h2(value).ellipsis()
236 };
237 card([card_content([
238 row([
239 row([
240 icon(icon_name)
241 .color(tokens::MUTED_FOREGROUND)
242 .icon_size(tokens::ICON_XS),
243 text(title).muted().ellipsis().width(Size::Fill(1.0)),
244 ])
245 .gap(tokens::SPACE_1)
246 .width(Size::Fill(1.0))
247 .align(Align::Center),
248 badge,
249 ])
250 .gap(tokens::SPACE_2)
251 .align(Align::Center),
252 value,
253 text(note).caption().ellipsis().width(Size::Fill(1.0)),
254 ])
255 .padding(tokens::SPACE_4)
256 .gap(tokens::SPACE_2)])
257 .key(if title == "Total Revenue" {
258 "metric:kpi.card"
259 } else {
260 title
261 })
262 .width(Size::Fill(1.0))
263}
264
265fn chart_card() -> El {
266 card([
267 card_header([
268 card_title("Visitors for the last 6 months"),
269 card_description("Total visitors by channel."),
270 ])
271 .padding(tokens::SPACE_4),
272 card_content([row(chart_bars())
273 .gap(2.0)
274 .height(Size::Fill(1.0))
275 .align(Align::End)])
276 .padding(Sides {
277 left: tokens::SPACE_4,
278 right: tokens::SPACE_4,
279 top: 0.0,
280 bottom: tokens::SPACE_4,
281 })
282 .height(Size::Fill(1.0)),
283 ])
284 .key("metric:chart.card")
285 .width(Size::Fill(1.0))
286 .height(Size::Fill(1.0))
287}
288
289fn chart_bars() -> Vec<El> {
290 [
291 48.0, 72.0, 56.0, 90.0, 64.0, 80.0, 108.0, 84.0, 122.0, 96.0, 136.0, 118.0,
292 ]
293 .into_iter()
294 .flat_map(|height| {
295 [
296 bar(height, tokens::MUTED_FOREGROUND),
297 bar((height - 28.0_f32).max(24.0), tokens::INPUT),
298 ]
299 })
300 .collect()
301}
302
303fn bar(height: f32, color: Color) -> El {
304 El::new(Kind::Custom("chart_bar"))
305 .fill(color)
306 .radius(tokens::RADIUS_SM)
307 .width(Size::Fill(1.0))
308 .height(Size::Fixed(height))
309}
310
311fn sales_card() -> El {
312 card([
313 card_header([
314 card_title("Recent Sales"),
315 card_description("You made 265 sales this month."),
316 ])
317 .padding(tokens::SPACE_4),
318 card_content([
319 sale_row("OM", "Olivia Martin", "olivia@example.com", "+$1,999.00"),
320 sale_row("JL", "Jackson Lee", "jackson@example.com", "+$39.00"),
321 sale_row("IN", "Isabella Nguyen", "isabella@example.com", "+$299.00"),
322 sale_row("WK", "William Kim", "will@example.com", "+$99.00"),
323 ])
324 .gap(tokens::SPACE_2)
325 .padding(Sides {
326 left: tokens::SPACE_4,
327 right: tokens::SPACE_4,
328 top: 0.0,
329 bottom: tokens::SPACE_4,
330 }),
331 ])
332 .key("metric:sales.card")
333 .width(Size::Fixed(330.0))
334 .height(Size::Fill(1.0))
335}
336
337fn sale_row(
338 initials: &'static str,
339 name: &'static str,
340 email: &'static str,
341 amount: &'static str,
342) -> El {
343 row([
344 icon_cell(initials),
345 column([
346 text(name).semibold().ellipsis().width(Size::Fill(1.0)),
347 text(email).caption().ellipsis().width(Size::Fill(1.0)),
348 ])
349 .gap(2.0)
350 .height(Size::Hug)
351 .width(Size::Fill(1.0)),
352 text(amount).label().small(),
353 ])
354 .gap(tokens::SPACE_2)
355 .height(Size::Fixed(42.0))
356 .align(Align::Center)
357}
358
359fn documents_card() -> El {
360 card([
361 card_header([card_title("Documents")]).padding(tokens::SPACE_4),
362 card_content([scroll([table([
363 table_header([table_row([
364 table_head("").width(Size::Fixed(35.0)),
365 table_head("Header").width(Size::Fill(1.8)),
366 table_head("Section Type").width(Size::Fill(1.0)),
367 table_head("Status").width(Size::Fixed(104.0)),
368 table_head("Target").width(Size::Fixed(64.0)),
369 table_head("Limit").width(Size::Fixed(64.0)),
370 table_head("Reviewer").width(Size::Fixed(128.0)),
371 table_head("").width(Size::Fixed(32.0)),
372 ])
373 .padding(Sides::xy(tokens::SPACE_4, 0.0))
374 .key("metric:table.header")]),
375 divider(),
376 table_body([
377 document_row(
378 "Cover page",
379 "Cover page",
380 "In Process",
381 "18",
382 "5",
383 "Eddie Lake",
384 "info",
385 ),
386 document_row(
387 "Table of contents",
388 "Table of contents",
389 "Done",
390 "29",
391 "24",
392 "Eddie Lake",
393 "success",
394 ),
395 ]),
396 ])])
397 .height(Size::Fill(1.0))])
398 .gap(0.0)
399 .padding(0.0)
400 .height(Size::Fill(1.0)),
401 ])
402 .key("metric:table.card")
403 .height(Size::Fill(1.0))
404}
405
406fn document_row(
407 header: &'static str,
408 section: &'static str,
409 status: &'static str,
410 target: &'static str,
411 limit: &'static str,
412 reviewer: &'static str,
413 tone: &'static str,
414) -> El {
415 let status_badge = match tone {
416 "success" => badge(status).success(),
417 _ => badge(status).info(),
418 };
419 table_row([
420 table_utility_cell("::"),
421 table_cell(text(header).label().small()).width(Size::Fill(1.8)),
422 table_cell(text(section).muted()).width(Size::Fill(1.0)),
423 table_cell(status_badge).width(Size::Fixed(104.0)),
424 table_cell(text(target).label().small()).width(Size::Fixed(64.0)),
425 table_cell(text(limit).label().small()).width(Size::Fixed(64.0)),
426 table_cell(text(reviewer).muted()).width(Size::Fixed(128.0)),
427 table_action_cell(),
428 ])
429 .padding(Sides::xy(tokens::SPACE_4, 0.0))
430 .key(if header == "Cover page" {
431 "metric:table.row"
432 } else {
433 header
434 })
435}
436
437fn table_utility_cell(label: &'static str) -> El {
438 table_cell(text(label).muted().center_text()).width(Size::Fixed(35.0))
439}
440
441fn table_action_cell() -> El {
442 stack([icon("more-horizontal")
443 .icon_size(tokens::ICON_SM)
444 .color(tokens::MUTED_FOREGROUND)])
445 .align(Align::Center)
446 .justify(Justify::Center)
447 .width(Size::Fixed(32.0))
448 .height(Size::Hug)
449}18fn fixture() -> El {
19 column([
20 h2("Inline runs"),
21 text_runs([
22 text("Aetna's attributed-text path lets you compose runs with "),
23 text("bold").bold(),
24 text(", "),
25 text("italic").italic(),
26 text(", "),
27 text("colored").color(tokens::DESTRUCTIVE),
28 text(", and "),
29 text("inline code").code(),
30 text(" segments inside one wrapping paragraph."),
31 hard_break(),
32 text("Hard breaks act like ").muted(),
33 text("<br>").code(),
34 text(" — they end the current line without breaking out of the run.").muted(),
35 ])
36 .wrap_text()
37 .width(Size::Fill(1.0))
38 .height(Size::Hug),
39 paragraph(
40 "All of the above flows through one cosmic-text rich-text shape — \
41 wrapping decisions cross run boundaries the way real prose wraps.",
42 )
43 .muted(),
44 ])
45 .gap(tokens::SPACE_4)
46 .padding(tokens::SPACE_7)
47 .width(Size::Fixed(640.0))
48}Source§impl El
impl El
Sourcepub fn text(self, t: impl Into<String>) -> Self
pub fn text(self, t: impl Into<String>) -> Self
Examples found in repository?
451fn icon_cell(label: &'static str) -> El {
452 El::new(Kind::Custom("icon_cell"))
453 .style_profile(StyleProfile::Surface)
454 .text(label)
455 .text_align(TextAlign::Center)
456 .caption()
457 .font_weight(FontWeight::Semibold)
458 .fill(tokens::MUTED)
459 .radius(tokens::RADIUS_SM)
460 .width(Size::Fixed(30.0))
461 .height(Size::Fixed(30.0))
462}More examples
355fn icon_cell(label: &'static str) -> El {
356 El::new(Kind::Custom("icon_cell"))
357 .style_profile(StyleProfile::Surface)
358 .text(label)
359 .text_align(TextAlign::Center)
360 .caption()
361 .font_weight(FontWeight::Semibold)
362 .fill(tokens::MUTED)
363 .stroke(tokens::BORDER)
364 .radius(tokens::RADIUS_SM)
365 .width(Size::Fixed(26.0))
366 .height(Size::Fixed(26.0))
367}Sourcepub fn text_color(self, c: Color) -> Self
pub fn text_color(self, c: Color) -> Self
Sourcepub fn text_align(self, align: TextAlign) -> Self
pub fn text_align(self, align: TextAlign) -> Self
Examples found in repository?
451fn icon_cell(label: &'static str) -> El {
452 El::new(Kind::Custom("icon_cell"))
453 .style_profile(StyleProfile::Surface)
454 .text(label)
455 .text_align(TextAlign::Center)
456 .caption()
457 .font_weight(FontWeight::Semibold)
458 .fill(tokens::MUTED)
459 .radius(tokens::RADIUS_SM)
460 .width(Size::Fixed(30.0))
461 .height(Size::Fixed(30.0))
462}More examples
355fn icon_cell(label: &'static str) -> El {
356 El::new(Kind::Custom("icon_cell"))
357 .style_profile(StyleProfile::Surface)
358 .text(label)
359 .text_align(TextAlign::Center)
360 .caption()
361 .font_weight(FontWeight::Semibold)
362 .fill(tokens::MUTED)
363 .stroke(tokens::BORDER)
364 .radius(tokens::RADIUS_SM)
365 .width(Size::Fixed(26.0))
366 .height(Size::Fixed(26.0))
367}Sourcepub fn center_text(self) -> Self
pub fn center_text(self) -> Self
Examples found in repository?
More examples
48fn fixture() -> El {
49 let centre = h2("Compass").center_text();
50 let dirs = [
51 ("North", "n"),
52 ("NE", "ne"),
53 ("East", "e"),
54 ("SE", "se"),
55 ("South", "s"),
56 ("SW", "sw"),
57 ("West", "w"),
58 ("NW", "nw"),
59 ];
60
61 let mut children: Vec<El> = vec![centre];
62 for (label, k) in dirs {
63 children.push(button(label).key(k).primary());
64 }
65
66 column([
67 h1("Custom layout — circular"),
68 paragraph(
69 "Eight buttons positioned on a circle by an author-supplied \
70 LayoutFn. Stock paint, automatic hover/press, and hit-test \
71 all keep working — only the rect distribution changed.",
72 )
73 .muted(),
74 stack(children)
75 .key("compass")
76 .layout(circular)
77 .width(Size::Fill(1.0))
78 .height(Size::Fixed(360.0)),
79 ])
80 .gap(tokens::SPACE_4)
81 .padding(tokens::SPACE_7)
82}pub fn end_text(self) -> Self
pub fn text_wrap(self, wrap: TextWrap) -> Self
Sourcepub fn wrap_text(self) -> Self
pub fn wrap_text(self) -> Self
Examples found in repository?
18fn fixture() -> El {
19 column([
20 h2("Inline runs"),
21 text_runs([
22 text("Aetna's attributed-text path lets you compose runs with "),
23 text("bold").bold(),
24 text(", "),
25 text("italic").italic(),
26 text(", "),
27 text("colored").color(tokens::DESTRUCTIVE),
28 text(", and "),
29 text("inline code").code(),
30 text(" segments inside one wrapping paragraph."),
31 hard_break(),
32 text("Hard breaks act like ").muted(),
33 text("<br>").code(),
34 text(" — they end the current line without breaking out of the run.").muted(),
35 ])
36 .wrap_text()
37 .width(Size::Fill(1.0))
38 .height(Size::Hug),
39 paragraph(
40 "All of the above flows through one cosmic-text rich-text shape — \
41 wrapping decisions cross run boundaries the way real prose wraps.",
42 )
43 .muted(),
44 ])
45 .gap(tokens::SPACE_4)
46 .padding(tokens::SPACE_7)
47 .width(Size::Fixed(640.0))
48}More examples
41fn settings_sidebar() -> El {
42 column([
43 row([
44 icon_slot("settings"),
45 column([
46 text("Workspace")
47 .semibold()
48 .ellipsis()
49 .width(Size::Fill(1.0)),
50 text("Settings").caption().ellipsis().width(Size::Fill(1.0)),
51 ])
52 .gap(2.0)
53 .width(Size::Fill(1.0))
54 .height(Size::Hug),
55 ])
56 .gap(tokens::SPACE_2)
57 .height(Size::Fixed(44.0))
58 .align(Align::Center),
59 section_label("Personal"),
60 side_item("users", "Profile", false),
61 side_item("settings", "Account", true),
62 side_item("alert-circle", "Security", false),
63 side_item("bell", "Notifications", false),
64 spacer().height(Size::Fixed(tokens::SPACE_4)),
65 section_label("Workspace"),
66 side_item("file-text", "Billing", false),
67 side_item("bar-chart", "Appearance", false),
68 side_item("activity", "Integrations", false),
69 spacer(),
70 column([text("Changes sync after save.").caption().wrap_text()])
71 .padding(tokens::SPACE_2)
72 .fill(tokens::MUTED)
73 .radius(tokens::RADIUS_MD),
74 ])
75 .gap(tokens::SPACE_2)
76 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
77 .key("metric:sidebar")
78 .width(Size::Fixed(244.0))
79 .height(Size::Fill(1.0))
80 .fill(tokens::CARD)
81 .stroke(tokens::BORDER)
82}
83
84fn settings_main() -> El {
85 column([
86 settings_header(),
87 row([settings_nav_card(), settings_body(), settings_aside()])
88 .gap(tokens::SPACE_4)
89 .padding(tokens::SPACE_4)
90 .height(Size::Fill(1.0))
91 .align(Align::Stretch),
92 ])
93 .width(Size::Fill(1.0))
94 .height(Size::Fill(1.0))
95}
96
97fn settings_header() -> El {
98 row([
99 icon_button("menu").ghost(),
100 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
101 h3("Settings").key("metric:page.title"),
102 spacer(),
103 button("Reset").secondary(),
104 button("Save changes").primary(),
105 ])
106 .key("metric:header")
107 .gap(tokens::SPACE_3)
108 .height(Size::Fixed(56.0))
109 .padding(Sides::xy(tokens::SPACE_4, 0.0))
110 .align(Align::Center)
111 .stroke(tokens::BORDER)
112}
113
114fn settings_nav_card() -> El {
115 column([
116 settings_nav_item("Account", true),
117 settings_nav_item("Security", false),
118 settings_nav_item("Notifications", false),
119 settings_nav_item("Appearance", false),
120 settings_nav_item("Billing", false),
121 ])
122 .gap(tokens::SPACE_1)
123 .padding(tokens::SPACE_1)
124 .width(Size::Fixed(220.0))
125 .height(Size::Fill(1.0))
126 .style_profile(StyleProfile::Surface)
127 .surface_role(SurfaceRole::Panel)
128 .fill(tokens::CARD)
129 .stroke(tokens::BORDER)
130 .radius(tokens::RADIUS_MD)
131 .shadow(tokens::SHADOW_MD)
132}
133
134fn settings_nav_item(label: &'static str, selected: bool) -> El {
135 let mut item = row([
136 El::new(Kind::Custom("nav-dot"))
137 .fill(tokens::MUTED_FOREGROUND)
138 .radius(tokens::RADIUS_PILL)
139 .width(Size::Fixed(6.0))
140 .height(Size::Fixed(6.0)),
141 text(label)
142 .font_weight(FontWeight::Medium)
143 .ellipsis()
144 .width(Size::Fill(1.0)),
145 ])
146 .key(if selected {
147 "metric:settings.nav.row".to_string()
148 } else {
149 format!("settings-nav-{label}")
150 })
151 .metrics_role(MetricsRole::ListItem)
152 .align(Align::Center)
153 .focusable();
154
155 if selected {
156 item = item.current();
157 } else {
158 item = item.color(tokens::MUTED_FOREGROUND);
159 }
160
161 item
162}
163
164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}26fn fixture() -> El {
27 column([
28 h2("Inline run backgrounds"),
29 paragraph(
30 "RunStyle.bg paints a per-line solid quad behind the glyphs of \
31 a styled span — the shaper computes the rect from the actual \
32 glyph extents, so wrapping splits the highlight cleanly.",
33 )
34 .muted(),
35 // Search-result style.
36 text_runs([
37 text("…the matcher finds "),
38 text("aetna").background(HIGHLIGHT_YELLOW).bold(),
39 text(" in "),
40 text("aetna_core::widgets").mono(),
41 text(" — the highlight tracks the glyph extent."),
42 ])
43 .wrap_text()
44 .width(Size::Fill(1.0))
45 .height(Size::Hug),
46 // Diff-style: add + remove tints inside the same line.
47 text_runs([
48 text("- "),
49 text("error::Custom").mono().background(DIFF_REMOVE),
50 text("(\"too narrow\")"),
51 hard_break(),
52 text("+ "),
53 text("error::WrapTooNarrow").mono().background(DIFF_ADD),
54 text(" { available }"),
55 ])
56 .wrap_text()
57 .width(Size::Fill(1.0))
58 .height(Size::Hug),
59 // Wrapping highlight: long span that spans two lines.
60 text_runs([
61 text("Long highlight: "),
62 text("the quick brown fox jumps over the lazy dog and keeps going")
63 .background(HIGHLIGHT_YELLOW),
64 text(" — the rect is split per line."),
65 ])
66 .wrap_text()
67 .width(Size::Fill(1.0))
68 .height(Size::Hug),
69 ])
70 .gap(tokens::SPACE_4)
71 .padding(tokens::SPACE_7)
72 .width(Size::Fixed(640.0))
73}Sourcepub fn nowrap_text(self) -> Self
pub fn nowrap_text(self) -> Self
Examples found in repository?
282fn token_chip(token: TokenDef, palette: &Palette) -> El {
283 let resolved = palette.resolve(token.color);
284 row([
285 El::new(Kind::Custom("palette-swatch"))
286 .at(file!(), line!())
287 .fill(token.color)
288 .stroke(tokens::BORDER)
289 .radius(tokens::RADIUS_SM)
290 .width(Size::Fixed(42.0))
291 .height(Size::Fixed(34.0)),
292 column([
293 text(token.name)
294 .label()
295 .ellipsis()
296 .nowrap_text()
297 .width(Size::Fill(1.0)),
298 mono(rgba_label(resolved)).caption().muted(),
299 ])
300 .gap(0.0)
301 .width(Size::Fill(1.0))
302 .height(Size::Hug),
303 ])
304 .gap(tokens::SPACE_2)
305 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_2))
306 .align(Align::Center)
307 .fill(tokens::CARD)
308 .stroke(tokens::BORDER)
309 .radius(tokens::RADIUS_MD)
310 .width(Size::Fill(1.0))
311 .height(Size::Fixed(54.0))
312}pub fn text_overflow(self, overflow: TextOverflow) -> Self
Sourcepub fn ellipsis(self) -> Self
pub fn ellipsis(self) -> Self
Examples found in repository?
67fn nav_item(icon: &'static str, label: &'static str, selected: bool) -> El {
68 let mut item = row([
69 icon_cell(icon),
70 text(label)
71 .font_weight(FontWeight::Medium)
72 .ellipsis()
73 .width(Size::Fill(1.0)),
74 ])
75 .key(if selected {
76 "metric:sidebar.nav.row".to_string()
77 } else {
78 format!("nav-{label}")
79 })
80 .metrics_role(MetricsRole::ListItem)
81 .gap(tokens::SPACE_3)
82 .padding(Sides::xy(tokens::SPACE_2, 0.0))
83 .height(Size::Fixed(40.0))
84 .align(Align::Center)
85 .focusable();
86
87 if selected {
88 item = item.current();
89 }
90
91 item
92}
93
94fn main_panel() -> El {
95 column([
96 toolbar(),
97 column([
98 row([
99 kpi_card("Latency", "42 ms", "-18%", true),
100 kpi_card("Runs", "1,284", "+12%", true),
101 kpi_card("Errors", "7", "+2", false),
102 ])
103 .gap(tokens::SPACE_4),
104 row([table_card(), command_card()])
105 .gap(tokens::SPACE_4)
106 .height(Size::Fill(1.0))
107 .align(Align::Stretch),
108 ])
109 .gap(tokens::SPACE_4)
110 .height(Size::Fill(1.0))
111 .align(Align::Stretch),
112 ])
113 .padding(tokens::SPACE_7)
114 .gap(tokens::SPACE_2)
115 .width(Size::Fill(1.0))
116 .height(Size::Fill(1.0))
117}
118
119fn toolbar() -> El {
120 row([
121 column([
122 h1("Polish calibration").key("metric:page.title"),
123 text("A representative app surface for default tuning.")
124 .muted()
125 .key("metric:page.subtitle"),
126 ])
127 .gap(tokens::SPACE_2)
128 .height(Size::Hug),
129 spacer(),
130 button_with_icon("search", "Preview")
131 .secondary()
132 .key("metric:action.secondary"),
133 button_with_icon("upload", "Publish")
134 .primary()
135 .key("metric:action.primary"),
136 ])
137 .key("metric:header")
138 .gap(tokens::SPACE_4)
139 .height(Size::Hug)
140 .align(Align::Start)
141}
142
143fn kpi_card(label: &'static str, value: &'static str, delta: &'static str, positive: bool) -> El {
144 let delta_badge = if positive {
145 badge(delta).success()
146 } else {
147 badge(delta).destructive()
148 };
149 let delta_badge = if label == "Latency" {
150 delta_badge.key("metric:kpi.badge")
151 } else {
152 delta_badge
153 };
154 let value_text = h2(value).display();
155 let value_text = if label == "Latency" {
156 value_text.key("metric:kpi.value")
157 } else {
158 value_text
159 };
160 card([
161 card_header([card_title(label)]),
162 card_content([
163 row([value_text, spacer(), delta_badge]).align(Align::Center),
164 text(if positive {
165 "Moving in the expected direction"
166 } else {
167 "Needs visual attention"
168 })
169 .muted(),
170 ])
171 .gap(tokens::SPACE_6),
172 ])
173 .key(if label == "Latency" {
174 "metric:kpi.card"
175 } else {
176 label
177 })
178 .width(Size::Fill(1.0))
179}
180
181fn table_card() -> El {
182 card([
183 card_header([card_title("Reference rows")]),
184 card_content([table([
185 table_header([table_row([
186 table_head("Status").width(Size::Fixed(86.0)),
187 table_head("Surface").width(Size::Fill(1.0)),
188 table_head("Owner").width(Size::Fixed(110.0)),
189 table_head("State").width(Size::Fixed(86.0)),
190 ])
191 .key("metric:table.header")]),
192 divider(),
193 table_body([
194 data_row("OK", "Settings card", "core", "selected", true, "success"),
195 data_row(
196 "WARN",
197 "Command palette density",
198 "widgets",
199 "needs work",
200 false,
201 "warning",
202 ),
203 data_row(
204 "ERR",
205 "Disabled and invalid states",
206 "style",
207 "missing",
208 false,
209 "destructive",
210 ),
211 data_row(
212 "INFO",
213 "Token resolution",
214 "theme",
215 "planned",
216 false,
217 "info",
218 ),
219 data_row(
220 "OK",
221 "Popover elevation",
222 "shader",
223 "queued",
224 false,
225 "success",
226 ),
227 ])
228 .gap(tokens::SPACE_1)
229 .width(Size::Fill(1.0)),
230 ])]),
231 ])
232 .key("metric:table.card")
233 .width(Size::Fill(1.2))
234 .height(Size::Fill(1.0))
235}
236
237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}More examples
282fn token_chip(token: TokenDef, palette: &Palette) -> El {
283 let resolved = palette.resolve(token.color);
284 row([
285 El::new(Kind::Custom("palette-swatch"))
286 .at(file!(), line!())
287 .fill(token.color)
288 .stroke(tokens::BORDER)
289 .radius(tokens::RADIUS_SM)
290 .width(Size::Fixed(42.0))
291 .height(Size::Fixed(34.0)),
292 column([
293 text(token.name)
294 .label()
295 .ellipsis()
296 .nowrap_text()
297 .width(Size::Fill(1.0)),
298 mono(rgba_label(resolved)).caption().muted(),
299 ])
300 .gap(0.0)
301 .width(Size::Fill(1.0))
302 .height(Size::Hug),
303 ])
304 .gap(tokens::SPACE_2)
305 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_2))
306 .align(Align::Center)
307 .fill(tokens::CARD)
308 .stroke(tokens::BORDER)
309 .radius(tokens::RADIUS_MD)
310 .width(Size::Fill(1.0))
311 .height(Size::Fixed(54.0))
312}41fn settings_sidebar() -> El {
42 column([
43 row([
44 icon_slot("settings"),
45 column([
46 text("Workspace")
47 .semibold()
48 .ellipsis()
49 .width(Size::Fill(1.0)),
50 text("Settings").caption().ellipsis().width(Size::Fill(1.0)),
51 ])
52 .gap(2.0)
53 .width(Size::Fill(1.0))
54 .height(Size::Hug),
55 ])
56 .gap(tokens::SPACE_2)
57 .height(Size::Fixed(44.0))
58 .align(Align::Center),
59 section_label("Personal"),
60 side_item("users", "Profile", false),
61 side_item("settings", "Account", true),
62 side_item("alert-circle", "Security", false),
63 side_item("bell", "Notifications", false),
64 spacer().height(Size::Fixed(tokens::SPACE_4)),
65 section_label("Workspace"),
66 side_item("file-text", "Billing", false),
67 side_item("bar-chart", "Appearance", false),
68 side_item("activity", "Integrations", false),
69 spacer(),
70 column([text("Changes sync after save.").caption().wrap_text()])
71 .padding(tokens::SPACE_2)
72 .fill(tokens::MUTED)
73 .radius(tokens::RADIUS_MD),
74 ])
75 .gap(tokens::SPACE_2)
76 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
77 .key("metric:sidebar")
78 .width(Size::Fixed(244.0))
79 .height(Size::Fill(1.0))
80 .fill(tokens::CARD)
81 .stroke(tokens::BORDER)
82}
83
84fn settings_main() -> El {
85 column([
86 settings_header(),
87 row([settings_nav_card(), settings_body(), settings_aside()])
88 .gap(tokens::SPACE_4)
89 .padding(tokens::SPACE_4)
90 .height(Size::Fill(1.0))
91 .align(Align::Stretch),
92 ])
93 .width(Size::Fill(1.0))
94 .height(Size::Fill(1.0))
95}
96
97fn settings_header() -> El {
98 row([
99 icon_button("menu").ghost(),
100 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
101 h3("Settings").key("metric:page.title"),
102 spacer(),
103 button("Reset").secondary(),
104 button("Save changes").primary(),
105 ])
106 .key("metric:header")
107 .gap(tokens::SPACE_3)
108 .height(Size::Fixed(56.0))
109 .padding(Sides::xy(tokens::SPACE_4, 0.0))
110 .align(Align::Center)
111 .stroke(tokens::BORDER)
112}
113
114fn settings_nav_card() -> El {
115 column([
116 settings_nav_item("Account", true),
117 settings_nav_item("Security", false),
118 settings_nav_item("Notifications", false),
119 settings_nav_item("Appearance", false),
120 settings_nav_item("Billing", false),
121 ])
122 .gap(tokens::SPACE_1)
123 .padding(tokens::SPACE_1)
124 .width(Size::Fixed(220.0))
125 .height(Size::Fill(1.0))
126 .style_profile(StyleProfile::Surface)
127 .surface_role(SurfaceRole::Panel)
128 .fill(tokens::CARD)
129 .stroke(tokens::BORDER)
130 .radius(tokens::RADIUS_MD)
131 .shadow(tokens::SHADOW_MD)
132}
133
134fn settings_nav_item(label: &'static str, selected: bool) -> El {
135 let mut item = row([
136 El::new(Kind::Custom("nav-dot"))
137 .fill(tokens::MUTED_FOREGROUND)
138 .radius(tokens::RADIUS_PILL)
139 .width(Size::Fixed(6.0))
140 .height(Size::Fixed(6.0)),
141 text(label)
142 .font_weight(FontWeight::Medium)
143 .ellipsis()
144 .width(Size::Fill(1.0)),
145 ])
146 .key(if selected {
147 "metric:settings.nav.row".to_string()
148 } else {
149 format!("settings-nav-{label}")
150 })
151 .metrics_role(MetricsRole::ListItem)
152 .align(Align::Center)
153 .focusable();
154
155 if selected {
156 item = item.current();
157 } else {
158 item = item.color(tokens::MUTED_FOREGROUND);
159 }
160
161 item
162}
163
164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}
185
186fn profile_card() -> El {
187 card([
188 card_header([
189 card_title("Profile"),
190 card_description("This information appears in audit logs and shared documents."),
191 ]),
192 card_content([form([
193 row([
194 setting_field("Display name", "Alicia Koch", "display-name"),
195 setting_field("Email", "alicia@acme.co", "email"),
196 ])
197 .gap(tokens::SPACE_3),
198 row([
199 setting_select("Role", "Workspace admin", "role"),
200 setting_select("Region", "US East", "region"),
201 ])
202 .gap(tokens::SPACE_3),
203 ])]),
204 ])
205 .key("metric:profile.card")
206}
207
208fn setting_field(label: &'static str, value: &'static str, key: &'static str) -> El {
209 form_item([
210 form_label(label),
211 form_control(
212 text_input(value, &Selection::caret(key, value.len()), key).key(
213 if key == "display-name" {
214 "metric:form.input"
215 } else {
216 key
217 },
218 ),
219 ),
220 ])
221 .width(Size::Fill(1.0))
222}
223
224fn setting_select(label: &'static str, value: &'static str, key: &'static str) -> El {
225 form_item([form_label(label), form_control(select_trigger(key, value))]).width(Size::Fill(1.0))
226}
227
228fn preferences_card() -> El {
229 card([
230 card_header([
231 card_title("Preferences"),
232 card_description("Defaults used when creating new dashboards and exports."),
233 ]),
234 card_content([column([
235 preference_row(
236 "Compact navigation",
237 "Use tighter rows in the sidebar and command menus.",
238 switch(true).key("compact-navigation"),
239 ),
240 divider(),
241 preference_row(
242 "Email summaries",
243 "Send a daily digest when documents change.",
244 switch(false).key("email-summaries"),
245 ),
246 divider(),
247 preference_row(
248 "Require approval",
249 "Route external sharing through an owner review.",
250 checkbox(true).key("approval-required"),
251 ),
252 ])
253 .gap(0.0)
254 .width(Size::Fill(1.0))])
255 .padding(0.0),
256 ])
257 .key("metric:preferences.card")
258}
259
260fn preference_row(title: &'static str, description: &'static str, control: El) -> El {
261 row([
262 column([
263 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
264 text(description)
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0))
271 .height(Size::Hug),
272 control,
273 ])
274 .key(if title == "Compact navigation" {
275 "metric:preference.row".to_string()
276 } else {
277 format!("preference-{title}")
278 })
279 .metrics_role(MetricsRole::PreferenceRow)
280 .gap(tokens::SPACE_4)
281 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_3))
282 .align(Align::Center)
283}
284
285fn settings_aside() -> El {
286 column([security_card(), scale_card()])
287 .gap(tokens::SPACE_4)
288 .width(Size::Fixed(300.0))
289 .height(Size::Fill(1.0))
290}
291
292fn security_card() -> El {
293 card([
294 card_header([
295 card_title("Security"),
296 card_description("Two-factor authentication is enabled for all privileged users."),
297 ]),
298 card_content([
299 compact_stat("Passkeys", "2 registered", badge("On").success()),
300 compact_stat("Sessions", "3 active", button("Review").secondary()),
301 ]),
302 ])
303 .width(Size::Fill(1.0))
304}
305
306fn scale_card() -> El {
307 card([
308 card_header([
309 card_title("Interface scale"),
310 card_description("Reference captures keep browser zoom fixed and vary root UI scale."),
311 ]),
312 card_content([
313 row([text("Dense").caption(), spacer(), text("Default").caption()]),
314 slider(0.66, tokens::PRIMARY)
315 .key("interface-scale")
316 .width(Size::Fill(1.0)),
317 ]),
318 ])
319 .width(Size::Fill(1.0))
320}
321
322fn compact_stat(title: &'static str, detail: &'static str, control: El) -> El {
323 row([
324 column([
325 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
326 text(detail).caption().ellipsis().width(Size::Fill(1.0)),
327 ])
328 .gap(2.0)
329 .width(Size::Fill(1.0))
330 .height(Size::Hug),
331 control,
332 ])
333 .gap(tokens::SPACE_2)
334 .height(Size::Fixed(44.0))
335 .align(Align::Center)
336}
337
338fn section_label(label: &'static str) -> El {
339 text(label)
340 .caption()
341 .height(Size::Fixed(22.0))
342 .padding(Sides::xy(tokens::SPACE_2, 0.0))
343}
344
345fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
346 let mut item = row([
347 icon(icon_name)
348 .color(tokens::MUTED_FOREGROUND)
349 .icon_size(tokens::ICON_SM)
350 .width(Size::Fixed(tokens::ICON_SM)),
351 text(label)
352 .font_weight(FontWeight::Medium)
353 .ellipsis()
354 .width(Size::Fill(1.0)),
355 ])
356 .key(if selected {
357 "metric:sidebar.nav.row".to_string()
358 } else {
359 format!("side-item-{label}")
360 })
361 .metrics_role(MetricsRole::ListItem)
362 .gap(tokens::SPACE_2)
363 .padding(Sides::xy(tokens::SPACE_2, 0.0))
364 .height(Size::Fixed(32.0))
365 .align(Align::Center)
366 .focusable();
367
368 if selected {
369 item = item.current();
370 } else {
371 item = item.color(tokens::MUTED_FOREGROUND);
372 }
373
374 item
375}41fn dashboard_sidebar() -> El {
42 column([
43 row([
44 icon_cell("A"),
45 column([
46 text("Acme Inc.")
47 .semibold()
48 .ellipsis()
49 .width(Size::Fill(1.0)),
50 text("Enterprise")
51 .caption()
52 .ellipsis()
53 .width(Size::Fill(1.0)),
54 ])
55 .gap(2.0)
56 .width(Size::Fill(1.0))
57 .height(Size::Hug),
58 ])
59 .gap(tokens::SPACE_2)
60 .height(Size::Fixed(44.0))
61 .align(Align::Center),
62 section_label("Platform"),
63 side_item("layout-dashboard", "Dashboard", true),
64 side_item("activity", "Lifecycle", false),
65 side_item("bar-chart", "Analytics", false),
66 side_item("folder", "Projects", false),
67 spacer().height(Size::Fixed(tokens::SPACE_4)),
68 section_label("Documents"),
69 side_item("file-text", "Data library", false),
70 side_item("download", "Reports", false),
71 side_item("users", "Team", false),
72 spacer(),
73 row([
74 icon_cell("AK"),
75 column([
76 text("Alicia Koch")
77 .semibold()
78 .ellipsis()
79 .width(Size::Fill(1.0)),
80 text("alicia@example.com")
81 .caption()
82 .ellipsis()
83 .width(Size::Fill(1.0)),
84 ])
85 .gap(2.0)
86 .width(Size::Fill(1.0))
87 .height(Size::Hug),
88 ])
89 .gap(tokens::SPACE_2)
90 .height(Size::Fixed(50.0))
91 .align(Align::Center),
92 ])
93 .gap(tokens::SPACE_2)
94 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
95 .key("metric:sidebar")
96 .width(Size::Fixed(244.0))
97 .height(Size::Fill(1.0))
98 .fill(tokens::CARD)
99 .stroke(tokens::BORDER)
100}
101
102fn section_label(label: &'static str) -> El {
103 text(label)
104 .caption()
105 .height(Size::Fixed(22.0))
106 .padding(Sides::xy(tokens::SPACE_2, 0.0))
107}
108
109fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
110 let mut item = row([
111 icon(icon_name)
112 .color(tokens::MUTED_FOREGROUND)
113 .icon_size(tokens::ICON_SM)
114 .width(Size::Fixed(tokens::ICON_SM)),
115 text(label)
116 .font_weight(FontWeight::Medium)
117 .ellipsis()
118 .width(Size::Fill(1.0)),
119 ])
120 .key(if selected {
121 "metric:sidebar.nav.row".to_string()
122 } else {
123 format!("side-item-{label}")
124 })
125 .metrics_role(MetricsRole::ListItem)
126 .gap(tokens::SPACE_2)
127 .padding(Sides::xy(tokens::SPACE_2, 0.0))
128 .height(Size::Fixed(32.0))
129 .align(Align::Center)
130 .focusable();
131
132 if selected {
133 item = item.current();
134 } else {
135 item = item.color(tokens::MUTED_FOREGROUND);
136 }
137
138 item
139}
140
141fn dashboard_main() -> El {
142 column([
143 dashboard_header(),
144 column([
145 row([
146 metric_card(
147 "bar-chart",
148 "Total Revenue",
149 "$1,250.00",
150 "+12.5%",
151 "Trending up this month",
152 true,
153 ),
154 metric_card(
155 "users",
156 "New Customers",
157 "1,234",
158 "-20%",
159 "Acquisition needs attention",
160 false,
161 ),
162 metric_card(
163 "folder",
164 "Active Accounts",
165 "45,678",
166 "+12.5%",
167 "Strong user retention",
168 true,
169 ),
170 metric_card(
171 "activity",
172 "Growth Rate",
173 "4.5%",
174 "+4.5%",
175 "Meets growth projections",
176 true,
177 ),
178 ])
179 .gap(tokens::SPACE_4),
180 row([chart_card(), sales_card()])
181 .gap(tokens::SPACE_4)
182 .height(Size::Fixed(306.0))
183 .align(Align::Stretch),
184 documents_card(),
185 ])
186 .gap(tokens::SPACE_4)
187 .padding(tokens::SPACE_7)
188 .height(Size::Fill(1.0)),
189 ])
190 .width(Size::Fill(1.0))
191 .height(Size::Fill(1.0))
192}
193
194fn dashboard_header() -> El {
195 row([
196 icon_button("menu").ghost(),
197 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
198 h3("Documents").key("metric:page.title"),
199 spacer(),
200 text_input("Search...", &Selection::default(), "dashboard-search")
201 .key("metric:command.input")
202 .width(Size::Fixed(260.0)),
203 icon_button("plus").ghost(),
204 icon_button("bell").ghost(),
205 ])
206 .key("metric:header")
207 .gap(tokens::SPACE_3)
208 .height(Size::Fixed(56.0))
209 .padding(Sides::xy(tokens::SPACE_4, 0.0))
210 .align(Align::Center)
211 .stroke(tokens::BORDER)
212}
213
214fn metric_card(
215 icon_name: &'static str,
216 title: &'static str,
217 value: &'static str,
218 delta: &'static str,
219 note: &'static str,
220 positive: bool,
221) -> El {
222 let badge = if positive {
223 badge(delta).success()
224 } else {
225 badge(delta).warning()
226 };
227 let badge = if title == "Total Revenue" {
228 badge.key("metric:kpi.badge")
229 } else {
230 badge
231 };
232 let value = if title == "Total Revenue" {
233 h2(value).ellipsis().key("metric:kpi.value")
234 } else {
235 h2(value).ellipsis()
236 };
237 card([card_content([
238 row([
239 row([
240 icon(icon_name)
241 .color(tokens::MUTED_FOREGROUND)
242 .icon_size(tokens::ICON_XS),
243 text(title).muted().ellipsis().width(Size::Fill(1.0)),
244 ])
245 .gap(tokens::SPACE_1)
246 .width(Size::Fill(1.0))
247 .align(Align::Center),
248 badge,
249 ])
250 .gap(tokens::SPACE_2)
251 .align(Align::Center),
252 value,
253 text(note).caption().ellipsis().width(Size::Fill(1.0)),
254 ])
255 .padding(tokens::SPACE_4)
256 .gap(tokens::SPACE_2)])
257 .key(if title == "Total Revenue" {
258 "metric:kpi.card"
259 } else {
260 title
261 })
262 .width(Size::Fill(1.0))
263}
264
265fn chart_card() -> El {
266 card([
267 card_header([
268 card_title("Visitors for the last 6 months"),
269 card_description("Total visitors by channel."),
270 ])
271 .padding(tokens::SPACE_4),
272 card_content([row(chart_bars())
273 .gap(2.0)
274 .height(Size::Fill(1.0))
275 .align(Align::End)])
276 .padding(Sides {
277 left: tokens::SPACE_4,
278 right: tokens::SPACE_4,
279 top: 0.0,
280 bottom: tokens::SPACE_4,
281 })
282 .height(Size::Fill(1.0)),
283 ])
284 .key("metric:chart.card")
285 .width(Size::Fill(1.0))
286 .height(Size::Fill(1.0))
287}
288
289fn chart_bars() -> Vec<El> {
290 [
291 48.0, 72.0, 56.0, 90.0, 64.0, 80.0, 108.0, 84.0, 122.0, 96.0, 136.0, 118.0,
292 ]
293 .into_iter()
294 .flat_map(|height| {
295 [
296 bar(height, tokens::MUTED_FOREGROUND),
297 bar((height - 28.0_f32).max(24.0), tokens::INPUT),
298 ]
299 })
300 .collect()
301}
302
303fn bar(height: f32, color: Color) -> El {
304 El::new(Kind::Custom("chart_bar"))
305 .fill(color)
306 .radius(tokens::RADIUS_SM)
307 .width(Size::Fill(1.0))
308 .height(Size::Fixed(height))
309}
310
311fn sales_card() -> El {
312 card([
313 card_header([
314 card_title("Recent Sales"),
315 card_description("You made 265 sales this month."),
316 ])
317 .padding(tokens::SPACE_4),
318 card_content([
319 sale_row("OM", "Olivia Martin", "olivia@example.com", "+$1,999.00"),
320 sale_row("JL", "Jackson Lee", "jackson@example.com", "+$39.00"),
321 sale_row("IN", "Isabella Nguyen", "isabella@example.com", "+$299.00"),
322 sale_row("WK", "William Kim", "will@example.com", "+$99.00"),
323 ])
324 .gap(tokens::SPACE_2)
325 .padding(Sides {
326 left: tokens::SPACE_4,
327 right: tokens::SPACE_4,
328 top: 0.0,
329 bottom: tokens::SPACE_4,
330 }),
331 ])
332 .key("metric:sales.card")
333 .width(Size::Fixed(330.0))
334 .height(Size::Fill(1.0))
335}
336
337fn sale_row(
338 initials: &'static str,
339 name: &'static str,
340 email: &'static str,
341 amount: &'static str,
342) -> El {
343 row([
344 icon_cell(initials),
345 column([
346 text(name).semibold().ellipsis().width(Size::Fill(1.0)),
347 text(email).caption().ellipsis().width(Size::Fill(1.0)),
348 ])
349 .gap(2.0)
350 .height(Size::Hug)
351 .width(Size::Fill(1.0)),
352 text(amount).label().small(),
353 ])
354 .gap(tokens::SPACE_2)
355 .height(Size::Fixed(42.0))
356 .align(Align::Center)
357}pub fn max_lines(self, lines: usize) -> Self
pub fn font_size(self, s: f32) -> Self
pub fn line_height(self, h: f32) -> Self
Sourcepub fn font_weight(self, w: FontWeight) -> Self
pub fn font_weight(self, w: FontWeight) -> Self
Examples found in repository?
67fn nav_item(icon: &'static str, label: &'static str, selected: bool) -> El {
68 let mut item = row([
69 icon_cell(icon),
70 text(label)
71 .font_weight(FontWeight::Medium)
72 .ellipsis()
73 .width(Size::Fill(1.0)),
74 ])
75 .key(if selected {
76 "metric:sidebar.nav.row".to_string()
77 } else {
78 format!("nav-{label}")
79 })
80 .metrics_role(MetricsRole::ListItem)
81 .gap(tokens::SPACE_3)
82 .padding(Sides::xy(tokens::SPACE_2, 0.0))
83 .height(Size::Fixed(40.0))
84 .align(Align::Center)
85 .focusable();
86
87 if selected {
88 item = item.current();
89 }
90
91 item
92}
93
94fn main_panel() -> El {
95 column([
96 toolbar(),
97 column([
98 row([
99 kpi_card("Latency", "42 ms", "-18%", true),
100 kpi_card("Runs", "1,284", "+12%", true),
101 kpi_card("Errors", "7", "+2", false),
102 ])
103 .gap(tokens::SPACE_4),
104 row([table_card(), command_card()])
105 .gap(tokens::SPACE_4)
106 .height(Size::Fill(1.0))
107 .align(Align::Stretch),
108 ])
109 .gap(tokens::SPACE_4)
110 .height(Size::Fill(1.0))
111 .align(Align::Stretch),
112 ])
113 .padding(tokens::SPACE_7)
114 .gap(tokens::SPACE_2)
115 .width(Size::Fill(1.0))
116 .height(Size::Fill(1.0))
117}
118
119fn toolbar() -> El {
120 row([
121 column([
122 h1("Polish calibration").key("metric:page.title"),
123 text("A representative app surface for default tuning.")
124 .muted()
125 .key("metric:page.subtitle"),
126 ])
127 .gap(tokens::SPACE_2)
128 .height(Size::Hug),
129 spacer(),
130 button_with_icon("search", "Preview")
131 .secondary()
132 .key("metric:action.secondary"),
133 button_with_icon("upload", "Publish")
134 .primary()
135 .key("metric:action.primary"),
136 ])
137 .key("metric:header")
138 .gap(tokens::SPACE_4)
139 .height(Size::Hug)
140 .align(Align::Start)
141}
142
143fn kpi_card(label: &'static str, value: &'static str, delta: &'static str, positive: bool) -> El {
144 let delta_badge = if positive {
145 badge(delta).success()
146 } else {
147 badge(delta).destructive()
148 };
149 let delta_badge = if label == "Latency" {
150 delta_badge.key("metric:kpi.badge")
151 } else {
152 delta_badge
153 };
154 let value_text = h2(value).display();
155 let value_text = if label == "Latency" {
156 value_text.key("metric:kpi.value")
157 } else {
158 value_text
159 };
160 card([
161 card_header([card_title(label)]),
162 card_content([
163 row([value_text, spacer(), delta_badge]).align(Align::Center),
164 text(if positive {
165 "Moving in the expected direction"
166 } else {
167 "Needs visual attention"
168 })
169 .muted(),
170 ])
171 .gap(tokens::SPACE_6),
172 ])
173 .key(if label == "Latency" {
174 "metric:kpi.card"
175 } else {
176 label
177 })
178 .width(Size::Fill(1.0))
179}
180
181fn table_card() -> El {
182 card([
183 card_header([card_title("Reference rows")]),
184 card_content([table([
185 table_header([table_row([
186 table_head("Status").width(Size::Fixed(86.0)),
187 table_head("Surface").width(Size::Fill(1.0)),
188 table_head("Owner").width(Size::Fixed(110.0)),
189 table_head("State").width(Size::Fixed(86.0)),
190 ])
191 .key("metric:table.header")]),
192 divider(),
193 table_body([
194 data_row("OK", "Settings card", "core", "selected", true, "success"),
195 data_row(
196 "WARN",
197 "Command palette density",
198 "widgets",
199 "needs work",
200 false,
201 "warning",
202 ),
203 data_row(
204 "ERR",
205 "Disabled and invalid states",
206 "style",
207 "missing",
208 false,
209 "destructive",
210 ),
211 data_row(
212 "INFO",
213 "Token resolution",
214 "theme",
215 "planned",
216 false,
217 "info",
218 ),
219 data_row(
220 "OK",
221 "Popover elevation",
222 "shader",
223 "queued",
224 false,
225 "success",
226 ),
227 ])
228 .gap(tokens::SPACE_1)
229 .width(Size::Fill(1.0)),
230 ])]),
231 ])
232 .key("metric:table.card")
233 .width(Size::Fill(1.2))
234 .height(Size::Fill(1.0))
235}
236
237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}
287
288fn command_card() -> El {
289 card([
290 card_header([card_title("Command surface")]),
291 card_content([
292 text_input(
293 "Search commands...",
294 &Selection::default(),
295 "command-search",
296 )
297 .key("metric:command.input")
298 .width(Size::Fill(1.0)),
299 popover_panel([
300 command_row("git-branch", "New branch", "Ctrl+B").key("metric:command.row"),
301 command_row("git-commit", "Commit staged files", "Ctrl+Enter")
302 .key("command-row-commit"),
303 command_row("refresh-cw", "Refresh repository", "Ctrl+R")
304 .key("command-row-refresh"),
305 command_row("alert-circle", "Force push", "Danger").key("command-row-force"),
306 ])
307 .width(Size::Fill(1.0)),
308 scroll([form_probe()]).key("form-probe-scroll"),
309 ])
310 .height(Size::Fill(1.0)),
311 ])
312 .key("metric:command.card")
313 .width(Size::Fill(0.8))
314 .height(Size::Fill(1.0))
315}
316
317fn form_probe() -> El {
318 form([
319 form_item([
320 form_label("Valid input"),
321 form_control(
322 text_input(
323 "Valid input",
324 &Selection::caret("valid-input", 11),
325 "valid-input",
326 )
327 .key("metric:form.input"),
328 ),
329 form_description("Default field spacing and helper text."),
330 ]),
331 form_item([
332 form_label("Invalid input"),
333 form_control(
334 text_input(
335 "Invalid input",
336 &Selection::caret("invalid-input", 13),
337 "invalid-input",
338 )
339 .invalid(),
340 ),
341 form_message("This field needs attention."),
342 ]),
343 row([
344 button("Disabled").secondary().disabled(),
345 button("Loading").primary().loading(),
346 spacer(),
347 ]),
348 ])
349 .padding(tokens::SPACE_3)
350 .fill(tokens::MUTED)
351 .stroke(tokens::BORDER)
352 .radius(tokens::RADIUS_MD)
353}
354
355fn icon_cell(label: &'static str) -> El {
356 El::new(Kind::Custom("icon_cell"))
357 .style_profile(StyleProfile::Surface)
358 .text(label)
359 .text_align(TextAlign::Center)
360 .caption()
361 .font_weight(FontWeight::Semibold)
362 .fill(tokens::MUTED)
363 .stroke(tokens::BORDER)
364 .radius(tokens::RADIUS_SM)
365 .width(Size::Fixed(26.0))
366 .height(Size::Fixed(26.0))
367}More examples
134fn settings_nav_item(label: &'static str, selected: bool) -> El {
135 let mut item = row([
136 El::new(Kind::Custom("nav-dot"))
137 .fill(tokens::MUTED_FOREGROUND)
138 .radius(tokens::RADIUS_PILL)
139 .width(Size::Fixed(6.0))
140 .height(Size::Fixed(6.0)),
141 text(label)
142 .font_weight(FontWeight::Medium)
143 .ellipsis()
144 .width(Size::Fill(1.0)),
145 ])
146 .key(if selected {
147 "metric:settings.nav.row".to_string()
148 } else {
149 format!("settings-nav-{label}")
150 })
151 .metrics_role(MetricsRole::ListItem)
152 .align(Align::Center)
153 .focusable();
154
155 if selected {
156 item = item.current();
157 } else {
158 item = item.color(tokens::MUTED_FOREGROUND);
159 }
160
161 item
162}
163
164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}
185
186fn profile_card() -> El {
187 card([
188 card_header([
189 card_title("Profile"),
190 card_description("This information appears in audit logs and shared documents."),
191 ]),
192 card_content([form([
193 row([
194 setting_field("Display name", "Alicia Koch", "display-name"),
195 setting_field("Email", "alicia@acme.co", "email"),
196 ])
197 .gap(tokens::SPACE_3),
198 row([
199 setting_select("Role", "Workspace admin", "role"),
200 setting_select("Region", "US East", "region"),
201 ])
202 .gap(tokens::SPACE_3),
203 ])]),
204 ])
205 .key("metric:profile.card")
206}
207
208fn setting_field(label: &'static str, value: &'static str, key: &'static str) -> El {
209 form_item([
210 form_label(label),
211 form_control(
212 text_input(value, &Selection::caret(key, value.len()), key).key(
213 if key == "display-name" {
214 "metric:form.input"
215 } else {
216 key
217 },
218 ),
219 ),
220 ])
221 .width(Size::Fill(1.0))
222}
223
224fn setting_select(label: &'static str, value: &'static str, key: &'static str) -> El {
225 form_item([form_label(label), form_control(select_trigger(key, value))]).width(Size::Fill(1.0))
226}
227
228fn preferences_card() -> El {
229 card([
230 card_header([
231 card_title("Preferences"),
232 card_description("Defaults used when creating new dashboards and exports."),
233 ]),
234 card_content([column([
235 preference_row(
236 "Compact navigation",
237 "Use tighter rows in the sidebar and command menus.",
238 switch(true).key("compact-navigation"),
239 ),
240 divider(),
241 preference_row(
242 "Email summaries",
243 "Send a daily digest when documents change.",
244 switch(false).key("email-summaries"),
245 ),
246 divider(),
247 preference_row(
248 "Require approval",
249 "Route external sharing through an owner review.",
250 checkbox(true).key("approval-required"),
251 ),
252 ])
253 .gap(0.0)
254 .width(Size::Fill(1.0))])
255 .padding(0.0),
256 ])
257 .key("metric:preferences.card")
258}
259
260fn preference_row(title: &'static str, description: &'static str, control: El) -> El {
261 row([
262 column([
263 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
264 text(description)
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0))
271 .height(Size::Hug),
272 control,
273 ])
274 .key(if title == "Compact navigation" {
275 "metric:preference.row".to_string()
276 } else {
277 format!("preference-{title}")
278 })
279 .metrics_role(MetricsRole::PreferenceRow)
280 .gap(tokens::SPACE_4)
281 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_3))
282 .align(Align::Center)
283}
284
285fn settings_aside() -> El {
286 column([security_card(), scale_card()])
287 .gap(tokens::SPACE_4)
288 .width(Size::Fixed(300.0))
289 .height(Size::Fill(1.0))
290}
291
292fn security_card() -> El {
293 card([
294 card_header([
295 card_title("Security"),
296 card_description("Two-factor authentication is enabled for all privileged users."),
297 ]),
298 card_content([
299 compact_stat("Passkeys", "2 registered", badge("On").success()),
300 compact_stat("Sessions", "3 active", button("Review").secondary()),
301 ]),
302 ])
303 .width(Size::Fill(1.0))
304}
305
306fn scale_card() -> El {
307 card([
308 card_header([
309 card_title("Interface scale"),
310 card_description("Reference captures keep browser zoom fixed and vary root UI scale."),
311 ]),
312 card_content([
313 row([text("Dense").caption(), spacer(), text("Default").caption()]),
314 slider(0.66, tokens::PRIMARY)
315 .key("interface-scale")
316 .width(Size::Fill(1.0)),
317 ]),
318 ])
319 .width(Size::Fill(1.0))
320}
321
322fn compact_stat(title: &'static str, detail: &'static str, control: El) -> El {
323 row([
324 column([
325 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
326 text(detail).caption().ellipsis().width(Size::Fill(1.0)),
327 ])
328 .gap(2.0)
329 .width(Size::Fill(1.0))
330 .height(Size::Hug),
331 control,
332 ])
333 .gap(tokens::SPACE_2)
334 .height(Size::Fixed(44.0))
335 .align(Align::Center)
336}
337
338fn section_label(label: &'static str) -> El {
339 text(label)
340 .caption()
341 .height(Size::Fixed(22.0))
342 .padding(Sides::xy(tokens::SPACE_2, 0.0))
343}
344
345fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
346 let mut item = row([
347 icon(icon_name)
348 .color(tokens::MUTED_FOREGROUND)
349 .icon_size(tokens::ICON_SM)
350 .width(Size::Fixed(tokens::ICON_SM)),
351 text(label)
352 .font_weight(FontWeight::Medium)
353 .ellipsis()
354 .width(Size::Fill(1.0)),
355 ])
356 .key(if selected {
357 "metric:sidebar.nav.row".to_string()
358 } else {
359 format!("side-item-{label}")
360 })
361 .metrics_role(MetricsRole::ListItem)
362 .gap(tokens::SPACE_2)
363 .padding(Sides::xy(tokens::SPACE_2, 0.0))
364 .height(Size::Fixed(32.0))
365 .align(Align::Center)
366 .focusable();
367
368 if selected {
369 item = item.current();
370 } else {
371 item = item.color(tokens::MUTED_FOREGROUND);
372 }
373
374 item
375}109fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
110 let mut item = row([
111 icon(icon_name)
112 .color(tokens::MUTED_FOREGROUND)
113 .icon_size(tokens::ICON_SM)
114 .width(Size::Fixed(tokens::ICON_SM)),
115 text(label)
116 .font_weight(FontWeight::Medium)
117 .ellipsis()
118 .width(Size::Fill(1.0)),
119 ])
120 .key(if selected {
121 "metric:sidebar.nav.row".to_string()
122 } else {
123 format!("side-item-{label}")
124 })
125 .metrics_role(MetricsRole::ListItem)
126 .gap(tokens::SPACE_2)
127 .padding(Sides::xy(tokens::SPACE_2, 0.0))
128 .height(Size::Fixed(32.0))
129 .align(Align::Center)
130 .focusable();
131
132 if selected {
133 item = item.current();
134 } else {
135 item = item.color(tokens::MUTED_FOREGROUND);
136 }
137
138 item
139}
140
141fn dashboard_main() -> El {
142 column([
143 dashboard_header(),
144 column([
145 row([
146 metric_card(
147 "bar-chart",
148 "Total Revenue",
149 "$1,250.00",
150 "+12.5%",
151 "Trending up this month",
152 true,
153 ),
154 metric_card(
155 "users",
156 "New Customers",
157 "1,234",
158 "-20%",
159 "Acquisition needs attention",
160 false,
161 ),
162 metric_card(
163 "folder",
164 "Active Accounts",
165 "45,678",
166 "+12.5%",
167 "Strong user retention",
168 true,
169 ),
170 metric_card(
171 "activity",
172 "Growth Rate",
173 "4.5%",
174 "+4.5%",
175 "Meets growth projections",
176 true,
177 ),
178 ])
179 .gap(tokens::SPACE_4),
180 row([chart_card(), sales_card()])
181 .gap(tokens::SPACE_4)
182 .height(Size::Fixed(306.0))
183 .align(Align::Stretch),
184 documents_card(),
185 ])
186 .gap(tokens::SPACE_4)
187 .padding(tokens::SPACE_7)
188 .height(Size::Fill(1.0)),
189 ])
190 .width(Size::Fill(1.0))
191 .height(Size::Fill(1.0))
192}
193
194fn dashboard_header() -> El {
195 row([
196 icon_button("menu").ghost(),
197 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
198 h3("Documents").key("metric:page.title"),
199 spacer(),
200 text_input("Search...", &Selection::default(), "dashboard-search")
201 .key("metric:command.input")
202 .width(Size::Fixed(260.0)),
203 icon_button("plus").ghost(),
204 icon_button("bell").ghost(),
205 ])
206 .key("metric:header")
207 .gap(tokens::SPACE_3)
208 .height(Size::Fixed(56.0))
209 .padding(Sides::xy(tokens::SPACE_4, 0.0))
210 .align(Align::Center)
211 .stroke(tokens::BORDER)
212}
213
214fn metric_card(
215 icon_name: &'static str,
216 title: &'static str,
217 value: &'static str,
218 delta: &'static str,
219 note: &'static str,
220 positive: bool,
221) -> El {
222 let badge = if positive {
223 badge(delta).success()
224 } else {
225 badge(delta).warning()
226 };
227 let badge = if title == "Total Revenue" {
228 badge.key("metric:kpi.badge")
229 } else {
230 badge
231 };
232 let value = if title == "Total Revenue" {
233 h2(value).ellipsis().key("metric:kpi.value")
234 } else {
235 h2(value).ellipsis()
236 };
237 card([card_content([
238 row([
239 row([
240 icon(icon_name)
241 .color(tokens::MUTED_FOREGROUND)
242 .icon_size(tokens::ICON_XS),
243 text(title).muted().ellipsis().width(Size::Fill(1.0)),
244 ])
245 .gap(tokens::SPACE_1)
246 .width(Size::Fill(1.0))
247 .align(Align::Center),
248 badge,
249 ])
250 .gap(tokens::SPACE_2)
251 .align(Align::Center),
252 value,
253 text(note).caption().ellipsis().width(Size::Fill(1.0)),
254 ])
255 .padding(tokens::SPACE_4)
256 .gap(tokens::SPACE_2)])
257 .key(if title == "Total Revenue" {
258 "metric:kpi.card"
259 } else {
260 title
261 })
262 .width(Size::Fill(1.0))
263}
264
265fn chart_card() -> El {
266 card([
267 card_header([
268 card_title("Visitors for the last 6 months"),
269 card_description("Total visitors by channel."),
270 ])
271 .padding(tokens::SPACE_4),
272 card_content([row(chart_bars())
273 .gap(2.0)
274 .height(Size::Fill(1.0))
275 .align(Align::End)])
276 .padding(Sides {
277 left: tokens::SPACE_4,
278 right: tokens::SPACE_4,
279 top: 0.0,
280 bottom: tokens::SPACE_4,
281 })
282 .height(Size::Fill(1.0)),
283 ])
284 .key("metric:chart.card")
285 .width(Size::Fill(1.0))
286 .height(Size::Fill(1.0))
287}
288
289fn chart_bars() -> Vec<El> {
290 [
291 48.0, 72.0, 56.0, 90.0, 64.0, 80.0, 108.0, 84.0, 122.0, 96.0, 136.0, 118.0,
292 ]
293 .into_iter()
294 .flat_map(|height| {
295 [
296 bar(height, tokens::MUTED_FOREGROUND),
297 bar((height - 28.0_f32).max(24.0), tokens::INPUT),
298 ]
299 })
300 .collect()
301}
302
303fn bar(height: f32, color: Color) -> El {
304 El::new(Kind::Custom("chart_bar"))
305 .fill(color)
306 .radius(tokens::RADIUS_SM)
307 .width(Size::Fill(1.0))
308 .height(Size::Fixed(height))
309}
310
311fn sales_card() -> El {
312 card([
313 card_header([
314 card_title("Recent Sales"),
315 card_description("You made 265 sales this month."),
316 ])
317 .padding(tokens::SPACE_4),
318 card_content([
319 sale_row("OM", "Olivia Martin", "olivia@example.com", "+$1,999.00"),
320 sale_row("JL", "Jackson Lee", "jackson@example.com", "+$39.00"),
321 sale_row("IN", "Isabella Nguyen", "isabella@example.com", "+$299.00"),
322 sale_row("WK", "William Kim", "will@example.com", "+$99.00"),
323 ])
324 .gap(tokens::SPACE_2)
325 .padding(Sides {
326 left: tokens::SPACE_4,
327 right: tokens::SPACE_4,
328 top: 0.0,
329 bottom: tokens::SPACE_4,
330 }),
331 ])
332 .key("metric:sales.card")
333 .width(Size::Fixed(330.0))
334 .height(Size::Fill(1.0))
335}
336
337fn sale_row(
338 initials: &'static str,
339 name: &'static str,
340 email: &'static str,
341 amount: &'static str,
342) -> El {
343 row([
344 icon_cell(initials),
345 column([
346 text(name).semibold().ellipsis().width(Size::Fill(1.0)),
347 text(email).caption().ellipsis().width(Size::Fill(1.0)),
348 ])
349 .gap(2.0)
350 .height(Size::Hug)
351 .width(Size::Fill(1.0)),
352 text(amount).label().small(),
353 ])
354 .gap(tokens::SPACE_2)
355 .height(Size::Fixed(42.0))
356 .align(Align::Center)
357}
358
359fn documents_card() -> El {
360 card([
361 card_header([card_title("Documents")]).padding(tokens::SPACE_4),
362 card_content([scroll([table([
363 table_header([table_row([
364 table_head("").width(Size::Fixed(35.0)),
365 table_head("Header").width(Size::Fill(1.8)),
366 table_head("Section Type").width(Size::Fill(1.0)),
367 table_head("Status").width(Size::Fixed(104.0)),
368 table_head("Target").width(Size::Fixed(64.0)),
369 table_head("Limit").width(Size::Fixed(64.0)),
370 table_head("Reviewer").width(Size::Fixed(128.0)),
371 table_head("").width(Size::Fixed(32.0)),
372 ])
373 .padding(Sides::xy(tokens::SPACE_4, 0.0))
374 .key("metric:table.header")]),
375 divider(),
376 table_body([
377 document_row(
378 "Cover page",
379 "Cover page",
380 "In Process",
381 "18",
382 "5",
383 "Eddie Lake",
384 "info",
385 ),
386 document_row(
387 "Table of contents",
388 "Table of contents",
389 "Done",
390 "29",
391 "24",
392 "Eddie Lake",
393 "success",
394 ),
395 ]),
396 ])])
397 .height(Size::Fill(1.0))])
398 .gap(0.0)
399 .padding(0.0)
400 .height(Size::Fill(1.0)),
401 ])
402 .key("metric:table.card")
403 .height(Size::Fill(1.0))
404}
405
406fn document_row(
407 header: &'static str,
408 section: &'static str,
409 status: &'static str,
410 target: &'static str,
411 limit: &'static str,
412 reviewer: &'static str,
413 tone: &'static str,
414) -> El {
415 let status_badge = match tone {
416 "success" => badge(status).success(),
417 _ => badge(status).info(),
418 };
419 table_row([
420 table_utility_cell("::"),
421 table_cell(text(header).label().small()).width(Size::Fill(1.8)),
422 table_cell(text(section).muted()).width(Size::Fill(1.0)),
423 table_cell(status_badge).width(Size::Fixed(104.0)),
424 table_cell(text(target).label().small()).width(Size::Fixed(64.0)),
425 table_cell(text(limit).label().small()).width(Size::Fixed(64.0)),
426 table_cell(text(reviewer).muted()).width(Size::Fixed(128.0)),
427 table_action_cell(),
428 ])
429 .padding(Sides::xy(tokens::SPACE_4, 0.0))
430 .key(if header == "Cover page" {
431 "metric:table.row"
432 } else {
433 header
434 })
435}
436
437fn table_utility_cell(label: &'static str) -> El {
438 table_cell(text(label).muted().center_text()).width(Size::Fixed(35.0))
439}
440
441fn table_action_cell() -> El {
442 stack([icon("more-horizontal")
443 .icon_size(tokens::ICON_SM)
444 .color(tokens::MUTED_FOREGROUND)])
445 .align(Align::Center)
446 .justify(Justify::Center)
447 .width(Size::Fixed(32.0))
448 .height(Size::Hug)
449}
450
451fn icon_cell(label: &'static str) -> El {
452 El::new(Kind::Custom("icon_cell"))
453 .style_profile(StyleProfile::Surface)
454 .text(label)
455 .text_align(TextAlign::Center)
456 .caption()
457 .font_weight(FontWeight::Semibold)
458 .fill(tokens::MUTED)
459 .radius(tokens::RADIUS_SM)
460 .width(Size::Fixed(30.0))
461 .height(Size::Fixed(30.0))
462}pub fn font_family(self, family: FontFamily) -> Self
pub fn inter(self) -> Self
pub fn roboto(self) -> Self
Sourcepub fn mono_font_family(self, family: FontFamily) -> Self
pub fn mono_font_family(self, family: FontFamily) -> Self
Override the monospace face used when this node renders as code
(font_mono = true, TextRole::Code, or any descendant that
inherits the value through theme propagation). Setting this
pins the node — theme with_mono_font_family(...) no longer
stamps over it.
Sourcepub fn jetbrains_mono(self) -> Self
pub fn jetbrains_mono(self) -> Self
Pin this node’s monospace face to JetBrains Mono. Convenience
shorthand for .mono_font_family(FontFamily::JetBrainsMono).
Sourcepub fn icon_source(self, source: impl IntoIconSource) -> Self
pub fn icon_source(self, source: impl IntoIconSource) -> Self
Set the icon for this element to either a built-in crate::IconName,
an app-supplied crate::SvgIcon, or a string-typed name from
the built-in vocabulary.
Sourcepub fn icon_name(self, source: impl IntoIconSource) -> Self
pub fn icon_name(self, source: impl IntoIconSource) -> Self
Convenience alias for Self::icon_source preserved for call
sites that want the historical name.
pub fn icon_stroke_width(self, width: f32) -> Self
Sourcepub fn icon_size(self, size: f32) -> Self
pub fn icon_size(self, size: f32) -> Self
Examples found in repository?
109fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
110 let mut item = row([
111 icon(icon_name)
112 .color(tokens::MUTED_FOREGROUND)
113 .icon_size(tokens::ICON_SM)
114 .width(Size::Fixed(tokens::ICON_SM)),
115 text(label)
116 .font_weight(FontWeight::Medium)
117 .ellipsis()
118 .width(Size::Fill(1.0)),
119 ])
120 .key(if selected {
121 "metric:sidebar.nav.row".to_string()
122 } else {
123 format!("side-item-{label}")
124 })
125 .metrics_role(MetricsRole::ListItem)
126 .gap(tokens::SPACE_2)
127 .padding(Sides::xy(tokens::SPACE_2, 0.0))
128 .height(Size::Fixed(32.0))
129 .align(Align::Center)
130 .focusable();
131
132 if selected {
133 item = item.current();
134 } else {
135 item = item.color(tokens::MUTED_FOREGROUND);
136 }
137
138 item
139}
140
141fn dashboard_main() -> El {
142 column([
143 dashboard_header(),
144 column([
145 row([
146 metric_card(
147 "bar-chart",
148 "Total Revenue",
149 "$1,250.00",
150 "+12.5%",
151 "Trending up this month",
152 true,
153 ),
154 metric_card(
155 "users",
156 "New Customers",
157 "1,234",
158 "-20%",
159 "Acquisition needs attention",
160 false,
161 ),
162 metric_card(
163 "folder",
164 "Active Accounts",
165 "45,678",
166 "+12.5%",
167 "Strong user retention",
168 true,
169 ),
170 metric_card(
171 "activity",
172 "Growth Rate",
173 "4.5%",
174 "+4.5%",
175 "Meets growth projections",
176 true,
177 ),
178 ])
179 .gap(tokens::SPACE_4),
180 row([chart_card(), sales_card()])
181 .gap(tokens::SPACE_4)
182 .height(Size::Fixed(306.0))
183 .align(Align::Stretch),
184 documents_card(),
185 ])
186 .gap(tokens::SPACE_4)
187 .padding(tokens::SPACE_7)
188 .height(Size::Fill(1.0)),
189 ])
190 .width(Size::Fill(1.0))
191 .height(Size::Fill(1.0))
192}
193
194fn dashboard_header() -> El {
195 row([
196 icon_button("menu").ghost(),
197 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
198 h3("Documents").key("metric:page.title"),
199 spacer(),
200 text_input("Search...", &Selection::default(), "dashboard-search")
201 .key("metric:command.input")
202 .width(Size::Fixed(260.0)),
203 icon_button("plus").ghost(),
204 icon_button("bell").ghost(),
205 ])
206 .key("metric:header")
207 .gap(tokens::SPACE_3)
208 .height(Size::Fixed(56.0))
209 .padding(Sides::xy(tokens::SPACE_4, 0.0))
210 .align(Align::Center)
211 .stroke(tokens::BORDER)
212}
213
214fn metric_card(
215 icon_name: &'static str,
216 title: &'static str,
217 value: &'static str,
218 delta: &'static str,
219 note: &'static str,
220 positive: bool,
221) -> El {
222 let badge = if positive {
223 badge(delta).success()
224 } else {
225 badge(delta).warning()
226 };
227 let badge = if title == "Total Revenue" {
228 badge.key("metric:kpi.badge")
229 } else {
230 badge
231 };
232 let value = if title == "Total Revenue" {
233 h2(value).ellipsis().key("metric:kpi.value")
234 } else {
235 h2(value).ellipsis()
236 };
237 card([card_content([
238 row([
239 row([
240 icon(icon_name)
241 .color(tokens::MUTED_FOREGROUND)
242 .icon_size(tokens::ICON_XS),
243 text(title).muted().ellipsis().width(Size::Fill(1.0)),
244 ])
245 .gap(tokens::SPACE_1)
246 .width(Size::Fill(1.0))
247 .align(Align::Center),
248 badge,
249 ])
250 .gap(tokens::SPACE_2)
251 .align(Align::Center),
252 value,
253 text(note).caption().ellipsis().width(Size::Fill(1.0)),
254 ])
255 .padding(tokens::SPACE_4)
256 .gap(tokens::SPACE_2)])
257 .key(if title == "Total Revenue" {
258 "metric:kpi.card"
259 } else {
260 title
261 })
262 .width(Size::Fill(1.0))
263}
264
265fn chart_card() -> El {
266 card([
267 card_header([
268 card_title("Visitors for the last 6 months"),
269 card_description("Total visitors by channel."),
270 ])
271 .padding(tokens::SPACE_4),
272 card_content([row(chart_bars())
273 .gap(2.0)
274 .height(Size::Fill(1.0))
275 .align(Align::End)])
276 .padding(Sides {
277 left: tokens::SPACE_4,
278 right: tokens::SPACE_4,
279 top: 0.0,
280 bottom: tokens::SPACE_4,
281 })
282 .height(Size::Fill(1.0)),
283 ])
284 .key("metric:chart.card")
285 .width(Size::Fill(1.0))
286 .height(Size::Fill(1.0))
287}
288
289fn chart_bars() -> Vec<El> {
290 [
291 48.0, 72.0, 56.0, 90.0, 64.0, 80.0, 108.0, 84.0, 122.0, 96.0, 136.0, 118.0,
292 ]
293 .into_iter()
294 .flat_map(|height| {
295 [
296 bar(height, tokens::MUTED_FOREGROUND),
297 bar((height - 28.0_f32).max(24.0), tokens::INPUT),
298 ]
299 })
300 .collect()
301}
302
303fn bar(height: f32, color: Color) -> El {
304 El::new(Kind::Custom("chart_bar"))
305 .fill(color)
306 .radius(tokens::RADIUS_SM)
307 .width(Size::Fill(1.0))
308 .height(Size::Fixed(height))
309}
310
311fn sales_card() -> El {
312 card([
313 card_header([
314 card_title("Recent Sales"),
315 card_description("You made 265 sales this month."),
316 ])
317 .padding(tokens::SPACE_4),
318 card_content([
319 sale_row("OM", "Olivia Martin", "olivia@example.com", "+$1,999.00"),
320 sale_row("JL", "Jackson Lee", "jackson@example.com", "+$39.00"),
321 sale_row("IN", "Isabella Nguyen", "isabella@example.com", "+$299.00"),
322 sale_row("WK", "William Kim", "will@example.com", "+$99.00"),
323 ])
324 .gap(tokens::SPACE_2)
325 .padding(Sides {
326 left: tokens::SPACE_4,
327 right: tokens::SPACE_4,
328 top: 0.0,
329 bottom: tokens::SPACE_4,
330 }),
331 ])
332 .key("metric:sales.card")
333 .width(Size::Fixed(330.0))
334 .height(Size::Fill(1.0))
335}
336
337fn sale_row(
338 initials: &'static str,
339 name: &'static str,
340 email: &'static str,
341 amount: &'static str,
342) -> El {
343 row([
344 icon_cell(initials),
345 column([
346 text(name).semibold().ellipsis().width(Size::Fill(1.0)),
347 text(email).caption().ellipsis().width(Size::Fill(1.0)),
348 ])
349 .gap(2.0)
350 .height(Size::Hug)
351 .width(Size::Fill(1.0)),
352 text(amount).label().small(),
353 ])
354 .gap(tokens::SPACE_2)
355 .height(Size::Fixed(42.0))
356 .align(Align::Center)
357}
358
359fn documents_card() -> El {
360 card([
361 card_header([card_title("Documents")]).padding(tokens::SPACE_4),
362 card_content([scroll([table([
363 table_header([table_row([
364 table_head("").width(Size::Fixed(35.0)),
365 table_head("Header").width(Size::Fill(1.8)),
366 table_head("Section Type").width(Size::Fill(1.0)),
367 table_head("Status").width(Size::Fixed(104.0)),
368 table_head("Target").width(Size::Fixed(64.0)),
369 table_head("Limit").width(Size::Fixed(64.0)),
370 table_head("Reviewer").width(Size::Fixed(128.0)),
371 table_head("").width(Size::Fixed(32.0)),
372 ])
373 .padding(Sides::xy(tokens::SPACE_4, 0.0))
374 .key("metric:table.header")]),
375 divider(),
376 table_body([
377 document_row(
378 "Cover page",
379 "Cover page",
380 "In Process",
381 "18",
382 "5",
383 "Eddie Lake",
384 "info",
385 ),
386 document_row(
387 "Table of contents",
388 "Table of contents",
389 "Done",
390 "29",
391 "24",
392 "Eddie Lake",
393 "success",
394 ),
395 ]),
396 ])])
397 .height(Size::Fill(1.0))])
398 .gap(0.0)
399 .padding(0.0)
400 .height(Size::Fill(1.0)),
401 ])
402 .key("metric:table.card")
403 .height(Size::Fill(1.0))
404}
405
406fn document_row(
407 header: &'static str,
408 section: &'static str,
409 status: &'static str,
410 target: &'static str,
411 limit: &'static str,
412 reviewer: &'static str,
413 tone: &'static str,
414) -> El {
415 let status_badge = match tone {
416 "success" => badge(status).success(),
417 _ => badge(status).info(),
418 };
419 table_row([
420 table_utility_cell("::"),
421 table_cell(text(header).label().small()).width(Size::Fill(1.8)),
422 table_cell(text(section).muted()).width(Size::Fill(1.0)),
423 table_cell(status_badge).width(Size::Fixed(104.0)),
424 table_cell(text(target).label().small()).width(Size::Fixed(64.0)),
425 table_cell(text(limit).label().small()).width(Size::Fixed(64.0)),
426 table_cell(text(reviewer).muted()).width(Size::Fixed(128.0)),
427 table_action_cell(),
428 ])
429 .padding(Sides::xy(tokens::SPACE_4, 0.0))
430 .key(if header == "Cover page" {
431 "metric:table.row"
432 } else {
433 header
434 })
435}
436
437fn table_utility_cell(label: &'static str) -> El {
438 table_cell(text(label).muted().center_text()).width(Size::Fixed(35.0))
439}
440
441fn table_action_cell() -> El {
442 stack([icon("more-horizontal")
443 .icon_size(tokens::ICON_SM)
444 .color(tokens::MUTED_FOREGROUND)])
445 .align(Align::Center)
446 .justify(Justify::Center)
447 .width(Size::Fixed(32.0))
448 .height(Size::Hug)
449}More examples
345fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
346 let mut item = row([
347 icon(icon_name)
348 .color(tokens::MUTED_FOREGROUND)
349 .icon_size(tokens::ICON_SM)
350 .width(Size::Fixed(tokens::ICON_SM)),
351 text(label)
352 .font_weight(FontWeight::Medium)
353 .ellipsis()
354 .width(Size::Fill(1.0)),
355 ])
356 .key(if selected {
357 "metric:sidebar.nav.row".to_string()
358 } else {
359 format!("side-item-{label}")
360 })
361 .metrics_role(MetricsRole::ListItem)
362 .gap(tokens::SPACE_2)
363 .padding(Sides::xy(tokens::SPACE_2, 0.0))
364 .height(Size::Fixed(32.0))
365 .align(Align::Center)
366 .focusable();
367
368 if selected {
369 item = item.current();
370 } else {
371 item = item.color(tokens::MUTED_FOREGROUND);
372 }
373
374 item
375}
376
377fn icon_slot(icon_name: &'static str) -> El {
378 El::new(Kind::Custom("icon_cell"))
379 .style_profile(StyleProfile::Surface)
380 .child(
381 icon(icon_name)
382 .color(tokens::FOREGROUND)
383 .icon_size(tokens::ICON_XS),
384 )
385 .align(Align::Center)
386 .justify(Justify::Center)
387 .fill(tokens::MUTED)
388 .stroke(tokens::BORDER)
389 .radius(tokens::RADIUS_SM)
390 .width(Size::Fixed(30.0))
391 .height(Size::Fixed(30.0))
392}Sourcepub fn image(self, image: impl Into<Image>) -> Self
pub fn image(self, image: impl Into<Image>) -> Self
Attach a raster image. Usually you’ll want the crate::image
free builder instead, which sets crate::Kind::Image for you; this
method exists for cases where you’ve already constructed an El
(e.g. through a stock widget) and want to swap in pixel art.
pub fn image_fit(self, fit: ImageFit) -> Self
pub fn image_tint(self, c: Color) -> Self
Sourcepub fn surface_source(self, source: SurfaceSource) -> Self
pub fn surface_source(self, source: SurfaceSource) -> Self
Attach an app-owned GPU texture source. Typically set via the
crate::tree::surface builder (which also sets
crate::Kind::Surface); reach for this method on a stock
widget El whose Kind you want to keep.
Sourcepub fn surface_alpha(self, alpha: SurfaceAlpha) -> Self
pub fn surface_alpha(self, alpha: SurfaceAlpha) -> Self
How a crate::Kind::Surface El composes with widgets below
it. Default is crate::surface::SurfaceAlpha::Premultiplied.
Sourcepub fn surface_fit(self, fit: ImageFit) -> Self
pub fn surface_fit(self, fit: ImageFit) -> Self
How a crate::Kind::Surface El’s texture projects into its
resolved rect. Defaults to crate::image::ImageFit::Fill —
stretch to the rect — for parity with the pre-surface_fit
behaviour. Contain / Cover / None mirror the modes on
crate::El::image_fit.
Sourcepub fn surface_transform(self, transform: Affine2) -> Self
pub fn surface_transform(self, transform: Affine2) -> Self
Affine applied to the texture quad in destination space, around
the centre of the post-Self::surface_fit rect. Defaults to
identity. Use this for rotation, mirroring, source-dimension-
independent zoom/pan, or any combination thereof. The El’s
auto-clip scissor still clamps the rendered content to the
resolved rect.
Sourcepub fn vector_source(self, asset: impl Into<Arc<VectorAsset>>) -> Self
pub fn vector_source(self, asset: impl Into<Arc<VectorAsset>>) -> Self
Attach a vector asset source. Typically set via the
crate::tree::vector builder (which also sets
crate::Kind::Vector); reach for this method on a stock
widget El whose Kind you want to keep.
Sourcepub fn vector_render_mode(self, mode: VectorRenderMode) -> Self
pub fn vector_render_mode(self, mode: VectorRenderMode) -> Self
Select how a vector asset should render. The default is
crate::vector::VectorRenderMode::Painted, which preserves
authored fills/strokes/gradients. Use Self::vector_mask when
the asset is intended as one-colour coverage geometry.
Sourcepub fn vector_mask(self, color: Color) -> Self
pub fn vector_mask(self, color: Color) -> Self
Treat this vector as coverage geometry and paint it with one colour. Backends can render this through their MSDF path.
Sourcepub fn vector_painted(self) -> Self
pub fn vector_painted(self) -> Self
Preserve authored vector paint. This is the default for
crate::tree::vector.
Sourcepub fn redraw_within(self, deadline: Duration) -> Self
pub fn redraw_within(self, deadline: Duration) -> Self
Inside-out redraw deadline. While this El is visible (rect
intersects the viewport), Aetna asks the host to drive the next
frame within deadline. Aggregated across the tree via min,
so the host gets a single signal regardless of how many widgets
are asking. Use Duration::ZERO for “next frame ASAP”;
non-zero values pace the redraw loop below the display rate.
Apps that pause / resume animation (e.g. GIF playback) just stop calling this method on the relevant El — Aetna re-runs the aggregation each frame, so the redraw scheduler quiets automatically when no visible widget is asking.
Sourcepub fn mono(self) -> Self
pub fn mono(self) -> Self
Opt this node into the monospace face. Setting this flag also
sets El::explicit_mono so a subsequent role modifier
(.caption() / .label() / .body() / .title() /
.heading() / .display()) won’t silently reset font_mono
when the role’s default is non-mono. The natural reading order
text(s).mono().caption() therefore renders in mono.
Examples found in repository?
20fn list_rows() -> Vec<El> {
21 (0..40)
22 .map(|i| {
23 row([
24 text(format!("{i:02}.")).mono().muted(),
25 text(format!("scrollable list item {i}")),
26 ])
27 .gap(tokens::SPACE_2)
28 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_1))
29 .height(Size::Fixed(28.0))
30 .align(Align::Center)
31 })
32 .collect()
33}
34
35fn fixture() -> El {
36 column([
37 h2("Scrollbar"),
38 text("scroll() and virtual_list() show a draggable thumb by default.").muted(),
39 row([
40 // 1) scroll() — default-on scrollbar.
41 column([
42 text("scroll() — default").bold(),
43 scroll(list_rows())
44 .height(Size::Fixed(240.0))
45 .padding(tokens::SPACE_2)
46 .stroke(tokens::BORDER)
47 .stroke_width(1.0)
48 .radius(tokens::RADIUS_MD),
49 ])
50 .gap(tokens::SPACE_2)
51 .width(Size::Fill(1.0))
52 .height(Size::Hug),
53 // 2) virtual_list — thumb scales to content size.
54 column([
55 text("virtual_list(200, 28)").bold(),
56 virtual_list(200, 28.0, |i| {
57 row([
58 text(format!("{i:03}")).mono().muted(),
59 text(format!("row {i}")),
60 ])
61 .gap(tokens::SPACE_2)
62 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_1))
63 .height(Size::Fixed(28.0))
64 .align(Align::Center)
65 })
66 .height(Size::Fixed(240.0))
67 .padding(tokens::SPACE_2)
68 .stroke(tokens::BORDER)
69 .stroke_width(1.0)
70 .radius(tokens::RADIUS_MD),
71 ])
72 .gap(tokens::SPACE_2)
73 .width(Size::Fill(1.0))
74 .height(Size::Hug),
75 // 3) Opt-out: same content, no thumb.
76 column([
77 text("scroll().no_scrollbar()").bold(),
78 scroll(list_rows())
79 .no_scrollbar()
80 .height(Size::Fixed(240.0))
81 .padding(tokens::SPACE_2)
82 .stroke(tokens::BORDER)
83 .stroke_width(1.0)
84 .radius(tokens::RADIUS_MD),
85 ])
86 .gap(tokens::SPACE_2)
87 .width(Size::Fill(1.0))
88 .height(Size::Hug),
89 ])
90 .gap(tokens::SPACE_4)
91 .width(Size::Fill(1.0)),
92 ])
93 .gap(tokens::SPACE_4)
94 .padding(tokens::SPACE_7)
95}More examples
68fn build_row(c: &FakeCommit, idx: usize, selected: bool) -> El {
69 row([
70 graph_cell(c.lane, selected),
71 text(c.sha).mono().muted(),
72 text(c.subject),
73 spacer(),
74 text(format!("{} · {}", c.author, c.when)).muted(),
75 ])
76 .key(format!("commit-{idx}"))
77 .gap(tokens::SPACE_3)
78 .padding(Sides::xy(tokens::SPACE_2, 0.0))
79 .height(Size::Fixed(ROW_HEIGHT))
80 .align(Align::Center)
81}18fn build_row(i: usize) -> El {
19 let badge_el = match i % 5 {
20 0 => badge("info").muted(),
21 1 => badge("warn").warning(),
22 2 => badge("ok").success(),
23 3 => badge("err").destructive(),
24 _ => spacer(),
25 };
26 row([
27 text(format!("#{i:05}")).mono(),
28 spacer(),
29 text(format!("entry {i}")),
30 spacer(),
31 badge_el,
32 ])
33 .key(format!("row-{i}"))
34 .gap(tokens::SPACE_3)
35 .padding(Sides::xy(tokens::SPACE_3, tokens::SPACE_2))
36 .height(Size::Fixed(ROW_HEIGHT))
37}26fn fixture() -> El {
27 column([
28 h2("Inline run backgrounds"),
29 paragraph(
30 "RunStyle.bg paints a per-line solid quad behind the glyphs of \
31 a styled span — the shaper computes the rect from the actual \
32 glyph extents, so wrapping splits the highlight cleanly.",
33 )
34 .muted(),
35 // Search-result style.
36 text_runs([
37 text("…the matcher finds "),
38 text("aetna").background(HIGHLIGHT_YELLOW).bold(),
39 text(" in "),
40 text("aetna_core::widgets").mono(),
41 text(" — the highlight tracks the glyph extent."),
42 ])
43 .wrap_text()
44 .width(Size::Fill(1.0))
45 .height(Size::Hug),
46 // Diff-style: add + remove tints inside the same line.
47 text_runs([
48 text("- "),
49 text("error::Custom").mono().background(DIFF_REMOVE),
50 text("(\"too narrow\")"),
51 hard_break(),
52 text("+ "),
53 text("error::WrapTooNarrow").mono().background(DIFF_ADD),
54 text(" { available }"),
55 ])
56 .wrap_text()
57 .width(Size::Fill(1.0))
58 .height(Size::Hug),
59 // Wrapping highlight: long span that spans two lines.
60 text_runs([
61 text("Long highlight: "),
62 text("the quick brown fox jumps over the lazy dog and keeps going")
63 .background(HIGHLIGHT_YELLOW),
64 text(" — the rect is split per line."),
65 ])
66 .wrap_text()
67 .width(Size::Fill(1.0))
68 .height(Size::Hug),
69 ])
70 .gap(tokens::SPACE_4)
71 .padding(tokens::SPACE_7)
72 .width(Size::Fixed(640.0))
73}Sourcepub fn italic(self) -> Self
pub fn italic(self) -> Self
Italic styling for a text run. Honoured by the
crate::Kind::Inlines layout pass and (best-effort) on
standalone text Els.
Examples found in repository?
18fn fixture() -> El {
19 column([
20 h2("Inline runs"),
21 text_runs([
22 text("Aetna's attributed-text path lets you compose runs with "),
23 text("bold").bold(),
24 text(", "),
25 text("italic").italic(),
26 text(", "),
27 text("colored").color(tokens::DESTRUCTIVE),
28 text(", and "),
29 text("inline code").code(),
30 text(" segments inside one wrapping paragraph."),
31 hard_break(),
32 text("Hard breaks act like ").muted(),
33 text("<br>").code(),
34 text(" — they end the current line without breaking out of the run.").muted(),
35 ])
36 .wrap_text()
37 .width(Size::Fill(1.0))
38 .height(Size::Hug),
39 paragraph(
40 "All of the above flows through one cosmic-text rich-text shape — \
41 wrapping decisions cross run boundaries the way real prose wraps.",
42 )
43 .muted(),
44 ])
45 .gap(tokens::SPACE_4)
46 .padding(tokens::SPACE_7)
47 .width(Size::Fixed(640.0))
48}More examples
15fn fixture() -> El {
16 column([
17 h2("Long-form content widgets"),
18 paragraph(
19 "These primitives compose the markdown-shaped vocabulary an \
20 upcoming transformer will target. Each widget is plain \
21 Aetna — selectable text, themed surfaces, the same layout \
22 pass as everything else.",
23 ),
24 h3("Highlights"),
25 bullet_list(vec![
26 text_runs([
27 text("Bulleted lists with a hanging indent — wrapped lines align under "),
28 text("themselves").italic(),
29 text(", not under the marker."),
30 ]),
31 text_runs([
32 text("Inline runs work inside list items: "),
33 text("bold").bold(),
34 text(", "),
35 text("code").code(),
36 text(", "),
37 text("links").link("https://aetna.dev"),
38 text("."),
39 ]),
40 text("Nested blocks live inside an item by composing a column."),
41 ]),
42 blockquote([
43 paragraph(
44 "Markdown's shape is HTML's shape. The Aetna widget kit \
45 already mirrors most of that shape, so the transformer \
46 mostly hands events to existing constructors.",
47 ),
48 paragraph("— Aetna design notes").muted(),
49 ]),
50 h3("Setup steps"),
51 numbered_list(vec![
52 text("Add `aetna-markdown` to the workspace."),
53 text_runs([
54 text("Pull in "),
55 text("pulldown-cmark").code(),
56 text(" with the GFM features the project actually uses."),
57 ]),
58 text("Wire the transformer through the existing widget kit — paragraph, list, blockquote, code_block, divider, table."),
59 ]),
60 h3("Example fenced block"),
61 code_block(
62 "fn render(md: &str) -> El {\n \
63 // pulldown-cmark events -> El\n \
64 todo!(\"phase 2\")\n}",
65 ),
66 ])
67 .gap(tokens::SPACE_4)
68 .padding(tokens::SPACE_7)
69 .width(Size::Fixed(640.0))
70}Sourcepub fn background(self, color: Color) -> Self
pub fn background(self, color: Color) -> Self
Inline-run background. Honoured when this El is a styled text
leaf inside an crate::Kind::Inlines parent: the shaped span
paints a solid quad behind its glyphs (per-line if the span
wraps). Mirrors HTML’s <mark> / inline background; the rect
tracks the glyph extent rather than the El’s layout box, so a
wrapped highlight follows the prose. No effect on standalone
text Els.
Examples found in repository?
26fn fixture() -> El {
27 column([
28 h2("Inline run backgrounds"),
29 paragraph(
30 "RunStyle.bg paints a per-line solid quad behind the glyphs of \
31 a styled span — the shaper computes the rect from the actual \
32 glyph extents, so wrapping splits the highlight cleanly.",
33 )
34 .muted(),
35 // Search-result style.
36 text_runs([
37 text("…the matcher finds "),
38 text("aetna").background(HIGHLIGHT_YELLOW).bold(),
39 text(" in "),
40 text("aetna_core::widgets").mono(),
41 text(" — the highlight tracks the glyph extent."),
42 ])
43 .wrap_text()
44 .width(Size::Fill(1.0))
45 .height(Size::Hug),
46 // Diff-style: add + remove tints inside the same line.
47 text_runs([
48 text("- "),
49 text("error::Custom").mono().background(DIFF_REMOVE),
50 text("(\"too narrow\")"),
51 hard_break(),
52 text("+ "),
53 text("error::WrapTooNarrow").mono().background(DIFF_ADD),
54 text(" { available }"),
55 ])
56 .wrap_text()
57 .width(Size::Fill(1.0))
58 .height(Size::Hug),
59 // Wrapping highlight: long span that spans two lines.
60 text_runs([
61 text("Long highlight: "),
62 text("the quick brown fox jumps over the lazy dog and keeps going")
63 .background(HIGHLIGHT_YELLOW),
64 text(" — the rect is split per line."),
65 ])
66 .wrap_text()
67 .width(Size::Fill(1.0))
68 .height(Size::Hug),
69 ])
70 .gap(tokens::SPACE_4)
71 .padding(tokens::SPACE_7)
72 .width(Size::Fixed(640.0))
73}Sourcepub fn strikethrough(self) -> Self
pub fn strikethrough(self) -> Self
Strikethrough styling for a text run.
Sourcepub fn code(self) -> Self
pub fn code(self) -> Self
Markdown-flavoured inline-code styling. Currently mono-styled;
a tinted background per the theme is a future addition. Authors
who want raw mono without code chrome should use Self::mono
instead.
Examples found in repository?
18fn fixture() -> El {
19 column([
20 h2("Inline runs"),
21 text_runs([
22 text("Aetna's attributed-text path lets you compose runs with "),
23 text("bold").bold(),
24 text(", "),
25 text("italic").italic(),
26 text(", "),
27 text("colored").color(tokens::DESTRUCTIVE),
28 text(", and "),
29 text("inline code").code(),
30 text(" segments inside one wrapping paragraph."),
31 hard_break(),
32 text("Hard breaks act like ").muted(),
33 text("<br>").code(),
34 text(" — they end the current line without breaking out of the run.").muted(),
35 ])
36 .wrap_text()
37 .width(Size::Fill(1.0))
38 .height(Size::Hug),
39 paragraph(
40 "All of the above flows through one cosmic-text rich-text shape — \
41 wrapping decisions cross run boundaries the way real prose wraps.",
42 )
43 .muted(),
44 ])
45 .gap(tokens::SPACE_4)
46 .padding(tokens::SPACE_7)
47 .width(Size::Fixed(640.0))
48}More examples
15fn fixture() -> El {
16 column([
17 h2("Long-form content widgets"),
18 paragraph(
19 "These primitives compose the markdown-shaped vocabulary an \
20 upcoming transformer will target. Each widget is plain \
21 Aetna — selectable text, themed surfaces, the same layout \
22 pass as everything else.",
23 ),
24 h3("Highlights"),
25 bullet_list(vec![
26 text_runs([
27 text("Bulleted lists with a hanging indent — wrapped lines align under "),
28 text("themselves").italic(),
29 text(", not under the marker."),
30 ]),
31 text_runs([
32 text("Inline runs work inside list items: "),
33 text("bold").bold(),
34 text(", "),
35 text("code").code(),
36 text(", "),
37 text("links").link("https://aetna.dev"),
38 text("."),
39 ]),
40 text("Nested blocks live inside an item by composing a column."),
41 ]),
42 blockquote([
43 paragraph(
44 "Markdown's shape is HTML's shape. The Aetna widget kit \
45 already mirrors most of that shape, so the transformer \
46 mostly hands events to existing constructors.",
47 ),
48 paragraph("— Aetna design notes").muted(),
49 ]),
50 h3("Setup steps"),
51 numbered_list(vec![
52 text("Add `aetna-markdown` to the workspace."),
53 text_runs([
54 text("Pull in "),
55 text("pulldown-cmark").code(),
56 text(" with the GFM features the project actually uses."),
57 ]),
58 text("Wire the transformer through the existing widget kit — paragraph, list, blockquote, code_block, divider, table."),
59 ]),
60 h3("Example fenced block"),
61 code_block(
62 "fn render(md: &str) -> El {\n \
63 // pulldown-cmark events -> El\n \
64 todo!(\"phase 2\")\n}",
65 ),
66 ])
67 .gap(tokens::SPACE_4)
68 .padding(tokens::SPACE_7)
69 .width(Size::Fixed(640.0))
70}Sourcepub fn link(self, url: impl Into<String>) -> Self
pub fn link(self, url: impl Into<String>) -> Self
Mark this run as a link to url. Inside an
crate::Kind::Inlines parent the run paints with a
link-themed color; runs sharing the same URL group together for
hit-test.
Examples found in repository?
15fn fixture() -> El {
16 column([
17 h2("Long-form content widgets"),
18 paragraph(
19 "These primitives compose the markdown-shaped vocabulary an \
20 upcoming transformer will target. Each widget is plain \
21 Aetna — selectable text, themed surfaces, the same layout \
22 pass as everything else.",
23 ),
24 h3("Highlights"),
25 bullet_list(vec![
26 text_runs([
27 text("Bulleted lists with a hanging indent — wrapped lines align under "),
28 text("themselves").italic(),
29 text(", not under the marker."),
30 ]),
31 text_runs([
32 text("Inline runs work inside list items: "),
33 text("bold").bold(),
34 text(", "),
35 text("code").code(),
36 text(", "),
37 text("links").link("https://aetna.dev"),
38 text("."),
39 ]),
40 text("Nested blocks live inside an item by composing a column."),
41 ]),
42 blockquote([
43 paragraph(
44 "Markdown's shape is HTML's shape. The Aetna widget kit \
45 already mirrors most of that shape, so the transformer \
46 mostly hands events to existing constructors.",
47 ),
48 paragraph("— Aetna design notes").muted(),
49 ]),
50 h3("Setup steps"),
51 numbered_list(vec![
52 text("Add `aetna-markdown` to the workspace."),
53 text_runs([
54 text("Pull in "),
55 text("pulldown-cmark").code(),
56 text(" with the GFM features the project actually uses."),
57 ]),
58 text("Wire the transformer through the existing widget kit — paragraph, list, blockquote, code_block, divider, table."),
59 ]),
60 h3("Example fenced block"),
61 code_block(
62 "fn render(md: &str) -> El {\n \
63 // pulldown-cmark events -> El\n \
64 todo!(\"phase 2\")\n}",
65 ),
66 ])
67 .gap(tokens::SPACE_4)
68 .padding(tokens::SPACE_7)
69 .width(Size::Fixed(640.0))
70}pub fn math_expr(self, expr: impl Into<Arc<MathExpr>>) -> Self
pub fn math_display(self, display: MathDisplay) -> Self
Source§impl El
impl El
Sourcepub fn new(kind: Kind) -> Self
pub fn new(kind: Kind) -> Self
Examples found in repository?
303fn bar(height: f32, color: Color) -> El {
304 El::new(Kind::Custom("chart_bar"))
305 .fill(color)
306 .radius(tokens::RADIUS_SM)
307 .width(Size::Fill(1.0))
308 .height(Size::Fixed(height))
309}
310
311fn sales_card() -> El {
312 card([
313 card_header([
314 card_title("Recent Sales"),
315 card_description("You made 265 sales this month."),
316 ])
317 .padding(tokens::SPACE_4),
318 card_content([
319 sale_row("OM", "Olivia Martin", "olivia@example.com", "+$1,999.00"),
320 sale_row("JL", "Jackson Lee", "jackson@example.com", "+$39.00"),
321 sale_row("IN", "Isabella Nguyen", "isabella@example.com", "+$299.00"),
322 sale_row("WK", "William Kim", "will@example.com", "+$99.00"),
323 ])
324 .gap(tokens::SPACE_2)
325 .padding(Sides {
326 left: tokens::SPACE_4,
327 right: tokens::SPACE_4,
328 top: 0.0,
329 bottom: tokens::SPACE_4,
330 }),
331 ])
332 .key("metric:sales.card")
333 .width(Size::Fixed(330.0))
334 .height(Size::Fill(1.0))
335}
336
337fn sale_row(
338 initials: &'static str,
339 name: &'static str,
340 email: &'static str,
341 amount: &'static str,
342) -> El {
343 row([
344 icon_cell(initials),
345 column([
346 text(name).semibold().ellipsis().width(Size::Fill(1.0)),
347 text(email).caption().ellipsis().width(Size::Fill(1.0)),
348 ])
349 .gap(2.0)
350 .height(Size::Hug)
351 .width(Size::Fill(1.0)),
352 text(amount).label().small(),
353 ])
354 .gap(tokens::SPACE_2)
355 .height(Size::Fixed(42.0))
356 .align(Align::Center)
357}
358
359fn documents_card() -> El {
360 card([
361 card_header([card_title("Documents")]).padding(tokens::SPACE_4),
362 card_content([scroll([table([
363 table_header([table_row([
364 table_head("").width(Size::Fixed(35.0)),
365 table_head("Header").width(Size::Fill(1.8)),
366 table_head("Section Type").width(Size::Fill(1.0)),
367 table_head("Status").width(Size::Fixed(104.0)),
368 table_head("Target").width(Size::Fixed(64.0)),
369 table_head("Limit").width(Size::Fixed(64.0)),
370 table_head("Reviewer").width(Size::Fixed(128.0)),
371 table_head("").width(Size::Fixed(32.0)),
372 ])
373 .padding(Sides::xy(tokens::SPACE_4, 0.0))
374 .key("metric:table.header")]),
375 divider(),
376 table_body([
377 document_row(
378 "Cover page",
379 "Cover page",
380 "In Process",
381 "18",
382 "5",
383 "Eddie Lake",
384 "info",
385 ),
386 document_row(
387 "Table of contents",
388 "Table of contents",
389 "Done",
390 "29",
391 "24",
392 "Eddie Lake",
393 "success",
394 ),
395 ]),
396 ])])
397 .height(Size::Fill(1.0))])
398 .gap(0.0)
399 .padding(0.0)
400 .height(Size::Fill(1.0)),
401 ])
402 .key("metric:table.card")
403 .height(Size::Fill(1.0))
404}
405
406fn document_row(
407 header: &'static str,
408 section: &'static str,
409 status: &'static str,
410 target: &'static str,
411 limit: &'static str,
412 reviewer: &'static str,
413 tone: &'static str,
414) -> El {
415 let status_badge = match tone {
416 "success" => badge(status).success(),
417 _ => badge(status).info(),
418 };
419 table_row([
420 table_utility_cell("::"),
421 table_cell(text(header).label().small()).width(Size::Fill(1.8)),
422 table_cell(text(section).muted()).width(Size::Fill(1.0)),
423 table_cell(status_badge).width(Size::Fixed(104.0)),
424 table_cell(text(target).label().small()).width(Size::Fixed(64.0)),
425 table_cell(text(limit).label().small()).width(Size::Fixed(64.0)),
426 table_cell(text(reviewer).muted()).width(Size::Fixed(128.0)),
427 table_action_cell(),
428 ])
429 .padding(Sides::xy(tokens::SPACE_4, 0.0))
430 .key(if header == "Cover page" {
431 "metric:table.row"
432 } else {
433 header
434 })
435}
436
437fn table_utility_cell(label: &'static str) -> El {
438 table_cell(text(label).muted().center_text()).width(Size::Fixed(35.0))
439}
440
441fn table_action_cell() -> El {
442 stack([icon("more-horizontal")
443 .icon_size(tokens::ICON_SM)
444 .color(tokens::MUTED_FOREGROUND)])
445 .align(Align::Center)
446 .justify(Justify::Center)
447 .width(Size::Fixed(32.0))
448 .height(Size::Hug)
449}
450
451fn icon_cell(label: &'static str) -> El {
452 El::new(Kind::Custom("icon_cell"))
453 .style_profile(StyleProfile::Surface)
454 .text(label)
455 .text_align(TextAlign::Center)
456 .caption()
457 .font_weight(FontWeight::Semibold)
458 .fill(tokens::MUTED)
459 .radius(tokens::RADIUS_SM)
460 .width(Size::Fixed(30.0))
461 .height(Size::Fixed(30.0))
462}More examples
355fn icon_cell(label: &'static str) -> El {
356 El::new(Kind::Custom("icon_cell"))
357 .style_profile(StyleProfile::Surface)
358 .text(label)
359 .text_align(TextAlign::Center)
360 .caption()
361 .font_weight(FontWeight::Semibold)
362 .fill(tokens::MUTED)
363 .stroke(tokens::BORDER)
364 .radius(tokens::RADIUS_SM)
365 .width(Size::Fixed(26.0))
366 .height(Size::Fixed(26.0))
367}44fn graph_cell(lane: u8, selected: bool) -> El {
45 let lane_color = lane_palette(lane);
46 let ring_color = if selected {
47 Color::rgb(245, 245, 250)
48 } else {
49 lane_color
50 };
51 let ring_w = if selected { 2.5 } else { 1.5 };
52 let radius = 5.0;
53 let line_w = 2.0;
54 let lane_frac = (lane as f32 + 0.5) / LANE_COUNT as f32;
55
56 El::new(Kind::Custom("graph_cell"))
57 .width(Size::Fixed(GRAPH_WIDTH))
58 .height(Size::Fixed(ROW_HEIGHT))
59 .shader(
60 ShaderBinding::custom("commit_node")
61 .color("vec_a", tokens::BACKGROUND)
62 .color("vec_b", ring_color)
63 .vec4("vec_c", [radius, ring_w, line_w, lane_frac]),
64 )
65 .fill(lane_color)
66}134fn settings_nav_item(label: &'static str, selected: bool) -> El {
135 let mut item = row([
136 El::new(Kind::Custom("nav-dot"))
137 .fill(tokens::MUTED_FOREGROUND)
138 .radius(tokens::RADIUS_PILL)
139 .width(Size::Fixed(6.0))
140 .height(Size::Fixed(6.0)),
141 text(label)
142 .font_weight(FontWeight::Medium)
143 .ellipsis()
144 .width(Size::Fill(1.0)),
145 ])
146 .key(if selected {
147 "metric:settings.nav.row".to_string()
148 } else {
149 format!("settings-nav-{label}")
150 })
151 .metrics_role(MetricsRole::ListItem)
152 .align(Align::Center)
153 .focusable();
154
155 if selected {
156 item = item.current();
157 } else {
158 item = item.color(tokens::MUTED_FOREGROUND);
159 }
160
161 item
162}
163
164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}
185
186fn profile_card() -> El {
187 card([
188 card_header([
189 card_title("Profile"),
190 card_description("This information appears in audit logs and shared documents."),
191 ]),
192 card_content([form([
193 row([
194 setting_field("Display name", "Alicia Koch", "display-name"),
195 setting_field("Email", "alicia@acme.co", "email"),
196 ])
197 .gap(tokens::SPACE_3),
198 row([
199 setting_select("Role", "Workspace admin", "role"),
200 setting_select("Region", "US East", "region"),
201 ])
202 .gap(tokens::SPACE_3),
203 ])]),
204 ])
205 .key("metric:profile.card")
206}
207
208fn setting_field(label: &'static str, value: &'static str, key: &'static str) -> El {
209 form_item([
210 form_label(label),
211 form_control(
212 text_input(value, &Selection::caret(key, value.len()), key).key(
213 if key == "display-name" {
214 "metric:form.input"
215 } else {
216 key
217 },
218 ),
219 ),
220 ])
221 .width(Size::Fill(1.0))
222}
223
224fn setting_select(label: &'static str, value: &'static str, key: &'static str) -> El {
225 form_item([form_label(label), form_control(select_trigger(key, value))]).width(Size::Fill(1.0))
226}
227
228fn preferences_card() -> El {
229 card([
230 card_header([
231 card_title("Preferences"),
232 card_description("Defaults used when creating new dashboards and exports."),
233 ]),
234 card_content([column([
235 preference_row(
236 "Compact navigation",
237 "Use tighter rows in the sidebar and command menus.",
238 switch(true).key("compact-navigation"),
239 ),
240 divider(),
241 preference_row(
242 "Email summaries",
243 "Send a daily digest when documents change.",
244 switch(false).key("email-summaries"),
245 ),
246 divider(),
247 preference_row(
248 "Require approval",
249 "Route external sharing through an owner review.",
250 checkbox(true).key("approval-required"),
251 ),
252 ])
253 .gap(0.0)
254 .width(Size::Fill(1.0))])
255 .padding(0.0),
256 ])
257 .key("metric:preferences.card")
258}
259
260fn preference_row(title: &'static str, description: &'static str, control: El) -> El {
261 row([
262 column([
263 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
264 text(description)
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0))
271 .height(Size::Hug),
272 control,
273 ])
274 .key(if title == "Compact navigation" {
275 "metric:preference.row".to_string()
276 } else {
277 format!("preference-{title}")
278 })
279 .metrics_role(MetricsRole::PreferenceRow)
280 .gap(tokens::SPACE_4)
281 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_3))
282 .align(Align::Center)
283}
284
285fn settings_aside() -> El {
286 column([security_card(), scale_card()])
287 .gap(tokens::SPACE_4)
288 .width(Size::Fixed(300.0))
289 .height(Size::Fill(1.0))
290}
291
292fn security_card() -> El {
293 card([
294 card_header([
295 card_title("Security"),
296 card_description("Two-factor authentication is enabled for all privileged users."),
297 ]),
298 card_content([
299 compact_stat("Passkeys", "2 registered", badge("On").success()),
300 compact_stat("Sessions", "3 active", button("Review").secondary()),
301 ]),
302 ])
303 .width(Size::Fill(1.0))
304}
305
306fn scale_card() -> El {
307 card([
308 card_header([
309 card_title("Interface scale"),
310 card_description("Reference captures keep browser zoom fixed and vary root UI scale."),
311 ]),
312 card_content([
313 row([text("Dense").caption(), spacer(), text("Default").caption()]),
314 slider(0.66, tokens::PRIMARY)
315 .key("interface-scale")
316 .width(Size::Fill(1.0)),
317 ]),
318 ])
319 .width(Size::Fill(1.0))
320}
321
322fn compact_stat(title: &'static str, detail: &'static str, control: El) -> El {
323 row([
324 column([
325 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
326 text(detail).caption().ellipsis().width(Size::Fill(1.0)),
327 ])
328 .gap(2.0)
329 .width(Size::Fill(1.0))
330 .height(Size::Hug),
331 control,
332 ])
333 .gap(tokens::SPACE_2)
334 .height(Size::Fixed(44.0))
335 .align(Align::Center)
336}
337
338fn section_label(label: &'static str) -> El {
339 text(label)
340 .caption()
341 .height(Size::Fixed(22.0))
342 .padding(Sides::xy(tokens::SPACE_2, 0.0))
343}
344
345fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
346 let mut item = row([
347 icon(icon_name)
348 .color(tokens::MUTED_FOREGROUND)
349 .icon_size(tokens::ICON_SM)
350 .width(Size::Fixed(tokens::ICON_SM)),
351 text(label)
352 .font_weight(FontWeight::Medium)
353 .ellipsis()
354 .width(Size::Fill(1.0)),
355 ])
356 .key(if selected {
357 "metric:sidebar.nav.row".to_string()
358 } else {
359 format!("side-item-{label}")
360 })
361 .metrics_role(MetricsRole::ListItem)
362 .gap(tokens::SPACE_2)
363 .padding(Sides::xy(tokens::SPACE_2, 0.0))
364 .height(Size::Fixed(32.0))
365 .align(Align::Center)
366 .focusable();
367
368 if selected {
369 item = item.current();
370 } else {
371 item = item.color(tokens::MUTED_FOREGROUND);
372 }
373
374 item
375}
376
377fn icon_slot(icon_name: &'static str) -> El {
378 El::new(Kind::Custom("icon_cell"))
379 .style_profile(StyleProfile::Surface)
380 .child(
381 icon(icon_name)
382 .color(tokens::FOREGROUND)
383 .icon_size(tokens::ICON_XS),
384 )
385 .align(Align::Center)
386 .justify(Justify::Center)
387 .fill(tokens::MUTED)
388 .stroke(tokens::BORDER)
389 .radius(tokens::RADIUS_SM)
390 .width(Size::Fixed(30.0))
391 .height(Size::Fixed(30.0))
392}282fn token_chip(token: TokenDef, palette: &Palette) -> El {
283 let resolved = palette.resolve(token.color);
284 row([
285 El::new(Kind::Custom("palette-swatch"))
286 .at(file!(), line!())
287 .fill(token.color)
288 .stroke(tokens::BORDER)
289 .radius(tokens::RADIUS_SM)
290 .width(Size::Fixed(42.0))
291 .height(Size::Fixed(34.0)),
292 column([
293 text(token.name)
294 .label()
295 .ellipsis()
296 .nowrap_text()
297 .width(Size::Fill(1.0)),
298 mono(rgba_label(resolved)).caption().muted(),
299 ])
300 .gap(0.0)
301 .width(Size::Fill(1.0))
302 .height(Size::Hug),
303 ])
304 .gap(tokens::SPACE_2)
305 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_2))
306 .align(Align::Center)
307 .fill(tokens::CARD)
308 .stroke(tokens::BORDER)
309 .radius(tokens::RADIUS_MD)
310 .width(Size::Fill(1.0))
311 .height(Size::Fixed(54.0))
312}Sourcepub fn key(self, k: impl Into<String>) -> Self
pub fn key(self, k: impl Into<String>) -> Self
Examples found in repository?
35fn polish_calibration() -> El {
36 row([sidebar(), main_panel()])
37 .key("metric:root")
38 .gap(0.0)
39 .fill_size()
40 .align(Align::Stretch)
41 .fill(tokens::BACKGROUND)
42}
43
44fn sidebar() -> El {
45 column([
46 column([h2("Aetna"), text("calibration").muted()])
47 .key("metric:sidebar.brand")
48 .gap(tokens::SPACE_1)
49 .height(Size::Hug),
50 spacer().height(Size::Fixed(tokens::SPACE_4)),
51 nav_item("01", "Overview", true),
52 nav_item("02", "Commands", false),
53 nav_item("03", "Tables", false),
54 nav_item("04", "Forms", false),
55 spacer(),
56 badge("dark theme").muted(),
57 ])
58 .gap(tokens::SPACE_2)
59 .padding(tokens::SPACE_5)
60 .key("metric:sidebar")
61 .width(Size::Fixed(220.0))
62 .height(Size::Fill(1.0))
63 .fill(tokens::CARD)
64 .stroke(tokens::BORDER)
65}
66
67fn nav_item(icon: &'static str, label: &'static str, selected: bool) -> El {
68 let mut item = row([
69 icon_cell(icon),
70 text(label)
71 .font_weight(FontWeight::Medium)
72 .ellipsis()
73 .width(Size::Fill(1.0)),
74 ])
75 .key(if selected {
76 "metric:sidebar.nav.row".to_string()
77 } else {
78 format!("nav-{label}")
79 })
80 .metrics_role(MetricsRole::ListItem)
81 .gap(tokens::SPACE_3)
82 .padding(Sides::xy(tokens::SPACE_2, 0.0))
83 .height(Size::Fixed(40.0))
84 .align(Align::Center)
85 .focusable();
86
87 if selected {
88 item = item.current();
89 }
90
91 item
92}
93
94fn main_panel() -> El {
95 column([
96 toolbar(),
97 column([
98 row([
99 kpi_card("Latency", "42 ms", "-18%", true),
100 kpi_card("Runs", "1,284", "+12%", true),
101 kpi_card("Errors", "7", "+2", false),
102 ])
103 .gap(tokens::SPACE_4),
104 row([table_card(), command_card()])
105 .gap(tokens::SPACE_4)
106 .height(Size::Fill(1.0))
107 .align(Align::Stretch),
108 ])
109 .gap(tokens::SPACE_4)
110 .height(Size::Fill(1.0))
111 .align(Align::Stretch),
112 ])
113 .padding(tokens::SPACE_7)
114 .gap(tokens::SPACE_2)
115 .width(Size::Fill(1.0))
116 .height(Size::Fill(1.0))
117}
118
119fn toolbar() -> El {
120 row([
121 column([
122 h1("Polish calibration").key("metric:page.title"),
123 text("A representative app surface for default tuning.")
124 .muted()
125 .key("metric:page.subtitle"),
126 ])
127 .gap(tokens::SPACE_2)
128 .height(Size::Hug),
129 spacer(),
130 button_with_icon("search", "Preview")
131 .secondary()
132 .key("metric:action.secondary"),
133 button_with_icon("upload", "Publish")
134 .primary()
135 .key("metric:action.primary"),
136 ])
137 .key("metric:header")
138 .gap(tokens::SPACE_4)
139 .height(Size::Hug)
140 .align(Align::Start)
141}
142
143fn kpi_card(label: &'static str, value: &'static str, delta: &'static str, positive: bool) -> El {
144 let delta_badge = if positive {
145 badge(delta).success()
146 } else {
147 badge(delta).destructive()
148 };
149 let delta_badge = if label == "Latency" {
150 delta_badge.key("metric:kpi.badge")
151 } else {
152 delta_badge
153 };
154 let value_text = h2(value).display();
155 let value_text = if label == "Latency" {
156 value_text.key("metric:kpi.value")
157 } else {
158 value_text
159 };
160 card([
161 card_header([card_title(label)]),
162 card_content([
163 row([value_text, spacer(), delta_badge]).align(Align::Center),
164 text(if positive {
165 "Moving in the expected direction"
166 } else {
167 "Needs visual attention"
168 })
169 .muted(),
170 ])
171 .gap(tokens::SPACE_6),
172 ])
173 .key(if label == "Latency" {
174 "metric:kpi.card"
175 } else {
176 label
177 })
178 .width(Size::Fill(1.0))
179}
180
181fn table_card() -> El {
182 card([
183 card_header([card_title("Reference rows")]),
184 card_content([table([
185 table_header([table_row([
186 table_head("Status").width(Size::Fixed(86.0)),
187 table_head("Surface").width(Size::Fill(1.0)),
188 table_head("Owner").width(Size::Fixed(110.0)),
189 table_head("State").width(Size::Fixed(86.0)),
190 ])
191 .key("metric:table.header")]),
192 divider(),
193 table_body([
194 data_row("OK", "Settings card", "core", "selected", true, "success"),
195 data_row(
196 "WARN",
197 "Command palette density",
198 "widgets",
199 "needs work",
200 false,
201 "warning",
202 ),
203 data_row(
204 "ERR",
205 "Disabled and invalid states",
206 "style",
207 "missing",
208 false,
209 "destructive",
210 ),
211 data_row(
212 "INFO",
213 "Token resolution",
214 "theme",
215 "planned",
216 false,
217 "info",
218 ),
219 data_row(
220 "OK",
221 "Popover elevation",
222 "shader",
223 "queued",
224 false,
225 "success",
226 ),
227 ])
228 .gap(tokens::SPACE_1)
229 .width(Size::Fill(1.0)),
230 ])]),
231 ])
232 .key("metric:table.card")
233 .width(Size::Fill(1.2))
234 .height(Size::Fill(1.0))
235}
236
237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}
287
288fn command_card() -> El {
289 card([
290 card_header([card_title("Command surface")]),
291 card_content([
292 text_input(
293 "Search commands...",
294 &Selection::default(),
295 "command-search",
296 )
297 .key("metric:command.input")
298 .width(Size::Fill(1.0)),
299 popover_panel([
300 command_row("git-branch", "New branch", "Ctrl+B").key("metric:command.row"),
301 command_row("git-commit", "Commit staged files", "Ctrl+Enter")
302 .key("command-row-commit"),
303 command_row("refresh-cw", "Refresh repository", "Ctrl+R")
304 .key("command-row-refresh"),
305 command_row("alert-circle", "Force push", "Danger").key("command-row-force"),
306 ])
307 .width(Size::Fill(1.0)),
308 scroll([form_probe()]).key("form-probe-scroll"),
309 ])
310 .height(Size::Fill(1.0)),
311 ])
312 .key("metric:command.card")
313 .width(Size::Fill(0.8))
314 .height(Size::Fill(1.0))
315}
316
317fn form_probe() -> El {
318 form([
319 form_item([
320 form_label("Valid input"),
321 form_control(
322 text_input(
323 "Valid input",
324 &Selection::caret("valid-input", 11),
325 "valid-input",
326 )
327 .key("metric:form.input"),
328 ),
329 form_description("Default field spacing and helper text."),
330 ]),
331 form_item([
332 form_label("Invalid input"),
333 form_control(
334 text_input(
335 "Invalid input",
336 &Selection::caret("invalid-input", 13),
337 "invalid-input",
338 )
339 .invalid(),
340 ),
341 form_message("This field needs attention."),
342 ]),
343 row([
344 button("Disabled").secondary().disabled(),
345 button("Loading").primary().loading(),
346 spacer(),
347 ]),
348 ])
349 .padding(tokens::SPACE_3)
350 .fill(tokens::MUTED)
351 .stroke(tokens::BORDER)
352 .radius(tokens::RADIUS_MD)
353}More examples
32fn settings_calibration() -> El {
33 row([settings_sidebar(), settings_main()])
34 .key("metric:root")
35 .gap(0.0)
36 .fill_size()
37 .align(Align::Stretch)
38 .fill(tokens::BACKGROUND)
39}
40
41fn settings_sidebar() -> El {
42 column([
43 row([
44 icon_slot("settings"),
45 column([
46 text("Workspace")
47 .semibold()
48 .ellipsis()
49 .width(Size::Fill(1.0)),
50 text("Settings").caption().ellipsis().width(Size::Fill(1.0)),
51 ])
52 .gap(2.0)
53 .width(Size::Fill(1.0))
54 .height(Size::Hug),
55 ])
56 .gap(tokens::SPACE_2)
57 .height(Size::Fixed(44.0))
58 .align(Align::Center),
59 section_label("Personal"),
60 side_item("users", "Profile", false),
61 side_item("settings", "Account", true),
62 side_item("alert-circle", "Security", false),
63 side_item("bell", "Notifications", false),
64 spacer().height(Size::Fixed(tokens::SPACE_4)),
65 section_label("Workspace"),
66 side_item("file-text", "Billing", false),
67 side_item("bar-chart", "Appearance", false),
68 side_item("activity", "Integrations", false),
69 spacer(),
70 column([text("Changes sync after save.").caption().wrap_text()])
71 .padding(tokens::SPACE_2)
72 .fill(tokens::MUTED)
73 .radius(tokens::RADIUS_MD),
74 ])
75 .gap(tokens::SPACE_2)
76 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
77 .key("metric:sidebar")
78 .width(Size::Fixed(244.0))
79 .height(Size::Fill(1.0))
80 .fill(tokens::CARD)
81 .stroke(tokens::BORDER)
82}
83
84fn settings_main() -> El {
85 column([
86 settings_header(),
87 row([settings_nav_card(), settings_body(), settings_aside()])
88 .gap(tokens::SPACE_4)
89 .padding(tokens::SPACE_4)
90 .height(Size::Fill(1.0))
91 .align(Align::Stretch),
92 ])
93 .width(Size::Fill(1.0))
94 .height(Size::Fill(1.0))
95}
96
97fn settings_header() -> El {
98 row([
99 icon_button("menu").ghost(),
100 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
101 h3("Settings").key("metric:page.title"),
102 spacer(),
103 button("Reset").secondary(),
104 button("Save changes").primary(),
105 ])
106 .key("metric:header")
107 .gap(tokens::SPACE_3)
108 .height(Size::Fixed(56.0))
109 .padding(Sides::xy(tokens::SPACE_4, 0.0))
110 .align(Align::Center)
111 .stroke(tokens::BORDER)
112}
113
114fn settings_nav_card() -> El {
115 column([
116 settings_nav_item("Account", true),
117 settings_nav_item("Security", false),
118 settings_nav_item("Notifications", false),
119 settings_nav_item("Appearance", false),
120 settings_nav_item("Billing", false),
121 ])
122 .gap(tokens::SPACE_1)
123 .padding(tokens::SPACE_1)
124 .width(Size::Fixed(220.0))
125 .height(Size::Fill(1.0))
126 .style_profile(StyleProfile::Surface)
127 .surface_role(SurfaceRole::Panel)
128 .fill(tokens::CARD)
129 .stroke(tokens::BORDER)
130 .radius(tokens::RADIUS_MD)
131 .shadow(tokens::SHADOW_MD)
132}
133
134fn settings_nav_item(label: &'static str, selected: bool) -> El {
135 let mut item = row([
136 El::new(Kind::Custom("nav-dot"))
137 .fill(tokens::MUTED_FOREGROUND)
138 .radius(tokens::RADIUS_PILL)
139 .width(Size::Fixed(6.0))
140 .height(Size::Fixed(6.0)),
141 text(label)
142 .font_weight(FontWeight::Medium)
143 .ellipsis()
144 .width(Size::Fill(1.0)),
145 ])
146 .key(if selected {
147 "metric:settings.nav.row".to_string()
148 } else {
149 format!("settings-nav-{label}")
150 })
151 .metrics_role(MetricsRole::ListItem)
152 .align(Align::Center)
153 .focusable();
154
155 if selected {
156 item = item.current();
157 } else {
158 item = item.color(tokens::MUTED_FOREGROUND);
159 }
160
161 item
162}
163
164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}
185
186fn profile_card() -> El {
187 card([
188 card_header([
189 card_title("Profile"),
190 card_description("This information appears in audit logs and shared documents."),
191 ]),
192 card_content([form([
193 row([
194 setting_field("Display name", "Alicia Koch", "display-name"),
195 setting_field("Email", "alicia@acme.co", "email"),
196 ])
197 .gap(tokens::SPACE_3),
198 row([
199 setting_select("Role", "Workspace admin", "role"),
200 setting_select("Region", "US East", "region"),
201 ])
202 .gap(tokens::SPACE_3),
203 ])]),
204 ])
205 .key("metric:profile.card")
206}
207
208fn setting_field(label: &'static str, value: &'static str, key: &'static str) -> El {
209 form_item([
210 form_label(label),
211 form_control(
212 text_input(value, &Selection::caret(key, value.len()), key).key(
213 if key == "display-name" {
214 "metric:form.input"
215 } else {
216 key
217 },
218 ),
219 ),
220 ])
221 .width(Size::Fill(1.0))
222}
223
224fn setting_select(label: &'static str, value: &'static str, key: &'static str) -> El {
225 form_item([form_label(label), form_control(select_trigger(key, value))]).width(Size::Fill(1.0))
226}
227
228fn preferences_card() -> El {
229 card([
230 card_header([
231 card_title("Preferences"),
232 card_description("Defaults used when creating new dashboards and exports."),
233 ]),
234 card_content([column([
235 preference_row(
236 "Compact navigation",
237 "Use tighter rows in the sidebar and command menus.",
238 switch(true).key("compact-navigation"),
239 ),
240 divider(),
241 preference_row(
242 "Email summaries",
243 "Send a daily digest when documents change.",
244 switch(false).key("email-summaries"),
245 ),
246 divider(),
247 preference_row(
248 "Require approval",
249 "Route external sharing through an owner review.",
250 checkbox(true).key("approval-required"),
251 ),
252 ])
253 .gap(0.0)
254 .width(Size::Fill(1.0))])
255 .padding(0.0),
256 ])
257 .key("metric:preferences.card")
258}
259
260fn preference_row(title: &'static str, description: &'static str, control: El) -> El {
261 row([
262 column([
263 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
264 text(description)
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0))
271 .height(Size::Hug),
272 control,
273 ])
274 .key(if title == "Compact navigation" {
275 "metric:preference.row".to_string()
276 } else {
277 format!("preference-{title}")
278 })
279 .metrics_role(MetricsRole::PreferenceRow)
280 .gap(tokens::SPACE_4)
281 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_3))
282 .align(Align::Center)
283}
284
285fn settings_aside() -> El {
286 column([security_card(), scale_card()])
287 .gap(tokens::SPACE_4)
288 .width(Size::Fixed(300.0))
289 .height(Size::Fill(1.0))
290}
291
292fn security_card() -> El {
293 card([
294 card_header([
295 card_title("Security"),
296 card_description("Two-factor authentication is enabled for all privileged users."),
297 ]),
298 card_content([
299 compact_stat("Passkeys", "2 registered", badge("On").success()),
300 compact_stat("Sessions", "3 active", button("Review").secondary()),
301 ]),
302 ])
303 .width(Size::Fill(1.0))
304}
305
306fn scale_card() -> El {
307 card([
308 card_header([
309 card_title("Interface scale"),
310 card_description("Reference captures keep browser zoom fixed and vary root UI scale."),
311 ]),
312 card_content([
313 row([text("Dense").caption(), spacer(), text("Default").caption()]),
314 slider(0.66, tokens::PRIMARY)
315 .key("interface-scale")
316 .width(Size::Fill(1.0)),
317 ]),
318 ])
319 .width(Size::Fill(1.0))
320}
321
322fn compact_stat(title: &'static str, detail: &'static str, control: El) -> El {
323 row([
324 column([
325 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
326 text(detail).caption().ellipsis().width(Size::Fill(1.0)),
327 ])
328 .gap(2.0)
329 .width(Size::Fill(1.0))
330 .height(Size::Hug),
331 control,
332 ])
333 .gap(tokens::SPACE_2)
334 .height(Size::Fixed(44.0))
335 .align(Align::Center)
336}
337
338fn section_label(label: &'static str) -> El {
339 text(label)
340 .caption()
341 .height(Size::Fixed(22.0))
342 .padding(Sides::xy(tokens::SPACE_2, 0.0))
343}
344
345fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
346 let mut item = row([
347 icon(icon_name)
348 .color(tokens::MUTED_FOREGROUND)
349 .icon_size(tokens::ICON_SM)
350 .width(Size::Fixed(tokens::ICON_SM)),
351 text(label)
352 .font_weight(FontWeight::Medium)
353 .ellipsis()
354 .width(Size::Fill(1.0)),
355 ])
356 .key(if selected {
357 "metric:sidebar.nav.row".to_string()
358 } else {
359 format!("side-item-{label}")
360 })
361 .metrics_role(MetricsRole::ListItem)
362 .gap(tokens::SPACE_2)
363 .padding(Sides::xy(tokens::SPACE_2, 0.0))
364 .height(Size::Fixed(32.0))
365 .align(Align::Center)
366 .focusable();
367
368 if selected {
369 item = item.current();
370 } else {
371 item = item.color(tokens::MUTED_FOREGROUND);
372 }
373
374 item
375}32fn dashboard_01_calibration() -> El {
33 row([dashboard_sidebar(), dashboard_main()])
34 .key("metric:root")
35 .gap(0.0)
36 .fill_size()
37 .align(Align::Stretch)
38 .fill(tokens::BACKGROUND)
39}
40
41fn dashboard_sidebar() -> El {
42 column([
43 row([
44 icon_cell("A"),
45 column([
46 text("Acme Inc.")
47 .semibold()
48 .ellipsis()
49 .width(Size::Fill(1.0)),
50 text("Enterprise")
51 .caption()
52 .ellipsis()
53 .width(Size::Fill(1.0)),
54 ])
55 .gap(2.0)
56 .width(Size::Fill(1.0))
57 .height(Size::Hug),
58 ])
59 .gap(tokens::SPACE_2)
60 .height(Size::Fixed(44.0))
61 .align(Align::Center),
62 section_label("Platform"),
63 side_item("layout-dashboard", "Dashboard", true),
64 side_item("activity", "Lifecycle", false),
65 side_item("bar-chart", "Analytics", false),
66 side_item("folder", "Projects", false),
67 spacer().height(Size::Fixed(tokens::SPACE_4)),
68 section_label("Documents"),
69 side_item("file-text", "Data library", false),
70 side_item("download", "Reports", false),
71 side_item("users", "Team", false),
72 spacer(),
73 row([
74 icon_cell("AK"),
75 column([
76 text("Alicia Koch")
77 .semibold()
78 .ellipsis()
79 .width(Size::Fill(1.0)),
80 text("alicia@example.com")
81 .caption()
82 .ellipsis()
83 .width(Size::Fill(1.0)),
84 ])
85 .gap(2.0)
86 .width(Size::Fill(1.0))
87 .height(Size::Hug),
88 ])
89 .gap(tokens::SPACE_2)
90 .height(Size::Fixed(50.0))
91 .align(Align::Center),
92 ])
93 .gap(tokens::SPACE_2)
94 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
95 .key("metric:sidebar")
96 .width(Size::Fixed(244.0))
97 .height(Size::Fill(1.0))
98 .fill(tokens::CARD)
99 .stroke(tokens::BORDER)
100}
101
102fn section_label(label: &'static str) -> El {
103 text(label)
104 .caption()
105 .height(Size::Fixed(22.0))
106 .padding(Sides::xy(tokens::SPACE_2, 0.0))
107}
108
109fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
110 let mut item = row([
111 icon(icon_name)
112 .color(tokens::MUTED_FOREGROUND)
113 .icon_size(tokens::ICON_SM)
114 .width(Size::Fixed(tokens::ICON_SM)),
115 text(label)
116 .font_weight(FontWeight::Medium)
117 .ellipsis()
118 .width(Size::Fill(1.0)),
119 ])
120 .key(if selected {
121 "metric:sidebar.nav.row".to_string()
122 } else {
123 format!("side-item-{label}")
124 })
125 .metrics_role(MetricsRole::ListItem)
126 .gap(tokens::SPACE_2)
127 .padding(Sides::xy(tokens::SPACE_2, 0.0))
128 .height(Size::Fixed(32.0))
129 .align(Align::Center)
130 .focusable();
131
132 if selected {
133 item = item.current();
134 } else {
135 item = item.color(tokens::MUTED_FOREGROUND);
136 }
137
138 item
139}
140
141fn dashboard_main() -> El {
142 column([
143 dashboard_header(),
144 column([
145 row([
146 metric_card(
147 "bar-chart",
148 "Total Revenue",
149 "$1,250.00",
150 "+12.5%",
151 "Trending up this month",
152 true,
153 ),
154 metric_card(
155 "users",
156 "New Customers",
157 "1,234",
158 "-20%",
159 "Acquisition needs attention",
160 false,
161 ),
162 metric_card(
163 "folder",
164 "Active Accounts",
165 "45,678",
166 "+12.5%",
167 "Strong user retention",
168 true,
169 ),
170 metric_card(
171 "activity",
172 "Growth Rate",
173 "4.5%",
174 "+4.5%",
175 "Meets growth projections",
176 true,
177 ),
178 ])
179 .gap(tokens::SPACE_4),
180 row([chart_card(), sales_card()])
181 .gap(tokens::SPACE_4)
182 .height(Size::Fixed(306.0))
183 .align(Align::Stretch),
184 documents_card(),
185 ])
186 .gap(tokens::SPACE_4)
187 .padding(tokens::SPACE_7)
188 .height(Size::Fill(1.0)),
189 ])
190 .width(Size::Fill(1.0))
191 .height(Size::Fill(1.0))
192}
193
194fn dashboard_header() -> El {
195 row([
196 icon_button("menu").ghost(),
197 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
198 h3("Documents").key("metric:page.title"),
199 spacer(),
200 text_input("Search...", &Selection::default(), "dashboard-search")
201 .key("metric:command.input")
202 .width(Size::Fixed(260.0)),
203 icon_button("plus").ghost(),
204 icon_button("bell").ghost(),
205 ])
206 .key("metric:header")
207 .gap(tokens::SPACE_3)
208 .height(Size::Fixed(56.0))
209 .padding(Sides::xy(tokens::SPACE_4, 0.0))
210 .align(Align::Center)
211 .stroke(tokens::BORDER)
212}
213
214fn metric_card(
215 icon_name: &'static str,
216 title: &'static str,
217 value: &'static str,
218 delta: &'static str,
219 note: &'static str,
220 positive: bool,
221) -> El {
222 let badge = if positive {
223 badge(delta).success()
224 } else {
225 badge(delta).warning()
226 };
227 let badge = if title == "Total Revenue" {
228 badge.key("metric:kpi.badge")
229 } else {
230 badge
231 };
232 let value = if title == "Total Revenue" {
233 h2(value).ellipsis().key("metric:kpi.value")
234 } else {
235 h2(value).ellipsis()
236 };
237 card([card_content([
238 row([
239 row([
240 icon(icon_name)
241 .color(tokens::MUTED_FOREGROUND)
242 .icon_size(tokens::ICON_XS),
243 text(title).muted().ellipsis().width(Size::Fill(1.0)),
244 ])
245 .gap(tokens::SPACE_1)
246 .width(Size::Fill(1.0))
247 .align(Align::Center),
248 badge,
249 ])
250 .gap(tokens::SPACE_2)
251 .align(Align::Center),
252 value,
253 text(note).caption().ellipsis().width(Size::Fill(1.0)),
254 ])
255 .padding(tokens::SPACE_4)
256 .gap(tokens::SPACE_2)])
257 .key(if title == "Total Revenue" {
258 "metric:kpi.card"
259 } else {
260 title
261 })
262 .width(Size::Fill(1.0))
263}
264
265fn chart_card() -> El {
266 card([
267 card_header([
268 card_title("Visitors for the last 6 months"),
269 card_description("Total visitors by channel."),
270 ])
271 .padding(tokens::SPACE_4),
272 card_content([row(chart_bars())
273 .gap(2.0)
274 .height(Size::Fill(1.0))
275 .align(Align::End)])
276 .padding(Sides {
277 left: tokens::SPACE_4,
278 right: tokens::SPACE_4,
279 top: 0.0,
280 bottom: tokens::SPACE_4,
281 })
282 .height(Size::Fill(1.0)),
283 ])
284 .key("metric:chart.card")
285 .width(Size::Fill(1.0))
286 .height(Size::Fill(1.0))
287}
288
289fn chart_bars() -> Vec<El> {
290 [
291 48.0, 72.0, 56.0, 90.0, 64.0, 80.0, 108.0, 84.0, 122.0, 96.0, 136.0, 118.0,
292 ]
293 .into_iter()
294 .flat_map(|height| {
295 [
296 bar(height, tokens::MUTED_FOREGROUND),
297 bar((height - 28.0_f32).max(24.0), tokens::INPUT),
298 ]
299 })
300 .collect()
301}
302
303fn bar(height: f32, color: Color) -> El {
304 El::new(Kind::Custom("chart_bar"))
305 .fill(color)
306 .radius(tokens::RADIUS_SM)
307 .width(Size::Fill(1.0))
308 .height(Size::Fixed(height))
309}
310
311fn sales_card() -> El {
312 card([
313 card_header([
314 card_title("Recent Sales"),
315 card_description("You made 265 sales this month."),
316 ])
317 .padding(tokens::SPACE_4),
318 card_content([
319 sale_row("OM", "Olivia Martin", "olivia@example.com", "+$1,999.00"),
320 sale_row("JL", "Jackson Lee", "jackson@example.com", "+$39.00"),
321 sale_row("IN", "Isabella Nguyen", "isabella@example.com", "+$299.00"),
322 sale_row("WK", "William Kim", "will@example.com", "+$99.00"),
323 ])
324 .gap(tokens::SPACE_2)
325 .padding(Sides {
326 left: tokens::SPACE_4,
327 right: tokens::SPACE_4,
328 top: 0.0,
329 bottom: tokens::SPACE_4,
330 }),
331 ])
332 .key("metric:sales.card")
333 .width(Size::Fixed(330.0))
334 .height(Size::Fill(1.0))
335}
336
337fn sale_row(
338 initials: &'static str,
339 name: &'static str,
340 email: &'static str,
341 amount: &'static str,
342) -> El {
343 row([
344 icon_cell(initials),
345 column([
346 text(name).semibold().ellipsis().width(Size::Fill(1.0)),
347 text(email).caption().ellipsis().width(Size::Fill(1.0)),
348 ])
349 .gap(2.0)
350 .height(Size::Hug)
351 .width(Size::Fill(1.0)),
352 text(amount).label().small(),
353 ])
354 .gap(tokens::SPACE_2)
355 .height(Size::Fixed(42.0))
356 .align(Align::Center)
357}
358
359fn documents_card() -> El {
360 card([
361 card_header([card_title("Documents")]).padding(tokens::SPACE_4),
362 card_content([scroll([table([
363 table_header([table_row([
364 table_head("").width(Size::Fixed(35.0)),
365 table_head("Header").width(Size::Fill(1.8)),
366 table_head("Section Type").width(Size::Fill(1.0)),
367 table_head("Status").width(Size::Fixed(104.0)),
368 table_head("Target").width(Size::Fixed(64.0)),
369 table_head("Limit").width(Size::Fixed(64.0)),
370 table_head("Reviewer").width(Size::Fixed(128.0)),
371 table_head("").width(Size::Fixed(32.0)),
372 ])
373 .padding(Sides::xy(tokens::SPACE_4, 0.0))
374 .key("metric:table.header")]),
375 divider(),
376 table_body([
377 document_row(
378 "Cover page",
379 "Cover page",
380 "In Process",
381 "18",
382 "5",
383 "Eddie Lake",
384 "info",
385 ),
386 document_row(
387 "Table of contents",
388 "Table of contents",
389 "Done",
390 "29",
391 "24",
392 "Eddie Lake",
393 "success",
394 ),
395 ]),
396 ])])
397 .height(Size::Fill(1.0))])
398 .gap(0.0)
399 .padding(0.0)
400 .height(Size::Fill(1.0)),
401 ])
402 .key("metric:table.card")
403 .height(Size::Fill(1.0))
404}
405
406fn document_row(
407 header: &'static str,
408 section: &'static str,
409 status: &'static str,
410 target: &'static str,
411 limit: &'static str,
412 reviewer: &'static str,
413 tone: &'static str,
414) -> El {
415 let status_badge = match tone {
416 "success" => badge(status).success(),
417 _ => badge(status).info(),
418 };
419 table_row([
420 table_utility_cell("::"),
421 table_cell(text(header).label().small()).width(Size::Fill(1.8)),
422 table_cell(text(section).muted()).width(Size::Fill(1.0)),
423 table_cell(status_badge).width(Size::Fixed(104.0)),
424 table_cell(text(target).label().small()).width(Size::Fixed(64.0)),
425 table_cell(text(limit).label().small()).width(Size::Fixed(64.0)),
426 table_cell(text(reviewer).muted()).width(Size::Fixed(128.0)),
427 table_action_cell(),
428 ])
429 .padding(Sides::xy(tokens::SPACE_4, 0.0))
430 .key(if header == "Cover page" {
431 "metric:table.row"
432 } else {
433 header
434 })
435}68fn build_row(c: &FakeCommit, idx: usize, selected: bool) -> El {
69 row([
70 graph_cell(c.lane, selected),
71 text(c.sha).mono().muted(),
72 text(c.subject),
73 spacer(),
74 text(format!("{} · {}", c.author, c.when)).muted(),
75 ])
76 .key(format!("commit-{idx}"))
77 .gap(tokens::SPACE_3)
78 .padding(Sides::xy(tokens::SPACE_2, 0.0))
79 .height(Size::Fixed(ROW_HEIGHT))
80 .align(Align::Center)
81}18fn build_row(i: usize) -> El {
19 let badge_el = match i % 5 {
20 0 => badge("info").muted(),
21 1 => badge("warn").warning(),
22 2 => badge("ok").success(),
23 3 => badge("err").destructive(),
24 _ => spacer(),
25 };
26 row([
27 text(format!("#{i:05}")).mono(),
28 spacer(),
29 text(format!("entry {i}")),
30 spacer(),
31 badge_el,
32 ])
33 .key(format!("row-{i}"))
34 .gap(tokens::SPACE_3)
35 .padding(Sides::xy(tokens::SPACE_3, tokens::SPACE_2))
36 .height(Size::Fixed(ROW_HEIGHT))
37}
38
39fn fixture() -> El {
40 column([
41 h1("Virtualized list"),
42 paragraph(format!(
43 "{ROW_COUNT} rows × {ROW_HEIGHT}px in a windowed viewport. \
44 Only the rows intersecting the viewport are realized — see \
45 `tree.txt` for proof."
46 ))
47 .muted(),
48 virtual_list(ROW_COUNT, ROW_HEIGHT, build_row)
49 .key("entries")
50 .height(Size::Fill(1.0)),
51 ])
52 .gap(tokens::SPACE_4)
53 .padding(tokens::SPACE_7)
54}15fn scroll_list_fixture() -> El {
16 let rows: Vec<El> = (0..20)
17 .map(|i| {
18 row([
19 badge(format!("#{i}")).info(),
20 text(format!("Notification {i}")).bold(),
21 spacer(),
22 text(format!("{}m ago", i + 1)).muted(),
23 ])
24 .gap(tokens::SPACE_2)
25 .height(Size::Fixed(44.0))
26 .padding(Sides::xy(tokens::SPACE_3, tokens::SPACE_2))
27 })
28 .collect();
29
30 let list = scroll(rows)
31 .key("notifications")
32 .height(Size::Fixed(420.0))
33 .padding(tokens::SPACE_2);
34
35 column([
36 h2("Notifications"),
37 text("Roll the wheel inside the panel to scroll. The content is taller than the viewport.")
38 .muted(),
39 list,
40 ])
41 .gap(tokens::SPACE_4)
42 .padding(tokens::SPACE_7)
43}pub fn block_pointer(self) -> Self
Sourcepub fn hit_overflow(self, outset: impl Into<Sides>) -> Self
pub fn hit_overflow(self, outset: impl Into<Sides>) -> Self
Expand this node’s pointer hit target without changing layout
or paint. Hover, press, cursor, tooltip, and click routing all
use the expanded target; UiEvent::target_rect
still reports the node’s transformed visual rect from layout.
Keep this conservative. It is for controls whose effective interaction region is intentionally larger than their drawn chrome, not for making unrelated gutters activate nearby UI.
Sourcepub fn focusable(self) -> Self
pub fn focusable(self) -> Self
Examples found in repository?
67fn nav_item(icon: &'static str, label: &'static str, selected: bool) -> El {
68 let mut item = row([
69 icon_cell(icon),
70 text(label)
71 .font_weight(FontWeight::Medium)
72 .ellipsis()
73 .width(Size::Fill(1.0)),
74 ])
75 .key(if selected {
76 "metric:sidebar.nav.row".to_string()
77 } else {
78 format!("nav-{label}")
79 })
80 .metrics_role(MetricsRole::ListItem)
81 .gap(tokens::SPACE_3)
82 .padding(Sides::xy(tokens::SPACE_2, 0.0))
83 .height(Size::Fixed(40.0))
84 .align(Align::Center)
85 .focusable();
86
87 if selected {
88 item = item.current();
89 }
90
91 item
92}
93
94fn main_panel() -> El {
95 column([
96 toolbar(),
97 column([
98 row([
99 kpi_card("Latency", "42 ms", "-18%", true),
100 kpi_card("Runs", "1,284", "+12%", true),
101 kpi_card("Errors", "7", "+2", false),
102 ])
103 .gap(tokens::SPACE_4),
104 row([table_card(), command_card()])
105 .gap(tokens::SPACE_4)
106 .height(Size::Fill(1.0))
107 .align(Align::Stretch),
108 ])
109 .gap(tokens::SPACE_4)
110 .height(Size::Fill(1.0))
111 .align(Align::Stretch),
112 ])
113 .padding(tokens::SPACE_7)
114 .gap(tokens::SPACE_2)
115 .width(Size::Fill(1.0))
116 .height(Size::Fill(1.0))
117}
118
119fn toolbar() -> El {
120 row([
121 column([
122 h1("Polish calibration").key("metric:page.title"),
123 text("A representative app surface for default tuning.")
124 .muted()
125 .key("metric:page.subtitle"),
126 ])
127 .gap(tokens::SPACE_2)
128 .height(Size::Hug),
129 spacer(),
130 button_with_icon("search", "Preview")
131 .secondary()
132 .key("metric:action.secondary"),
133 button_with_icon("upload", "Publish")
134 .primary()
135 .key("metric:action.primary"),
136 ])
137 .key("metric:header")
138 .gap(tokens::SPACE_4)
139 .height(Size::Hug)
140 .align(Align::Start)
141}
142
143fn kpi_card(label: &'static str, value: &'static str, delta: &'static str, positive: bool) -> El {
144 let delta_badge = if positive {
145 badge(delta).success()
146 } else {
147 badge(delta).destructive()
148 };
149 let delta_badge = if label == "Latency" {
150 delta_badge.key("metric:kpi.badge")
151 } else {
152 delta_badge
153 };
154 let value_text = h2(value).display();
155 let value_text = if label == "Latency" {
156 value_text.key("metric:kpi.value")
157 } else {
158 value_text
159 };
160 card([
161 card_header([card_title(label)]),
162 card_content([
163 row([value_text, spacer(), delta_badge]).align(Align::Center),
164 text(if positive {
165 "Moving in the expected direction"
166 } else {
167 "Needs visual attention"
168 })
169 .muted(),
170 ])
171 .gap(tokens::SPACE_6),
172 ])
173 .key(if label == "Latency" {
174 "metric:kpi.card"
175 } else {
176 label
177 })
178 .width(Size::Fill(1.0))
179}
180
181fn table_card() -> El {
182 card([
183 card_header([card_title("Reference rows")]),
184 card_content([table([
185 table_header([table_row([
186 table_head("Status").width(Size::Fixed(86.0)),
187 table_head("Surface").width(Size::Fill(1.0)),
188 table_head("Owner").width(Size::Fixed(110.0)),
189 table_head("State").width(Size::Fixed(86.0)),
190 ])
191 .key("metric:table.header")]),
192 divider(),
193 table_body([
194 data_row("OK", "Settings card", "core", "selected", true, "success"),
195 data_row(
196 "WARN",
197 "Command palette density",
198 "widgets",
199 "needs work",
200 false,
201 "warning",
202 ),
203 data_row(
204 "ERR",
205 "Disabled and invalid states",
206 "style",
207 "missing",
208 false,
209 "destructive",
210 ),
211 data_row(
212 "INFO",
213 "Token resolution",
214 "theme",
215 "planned",
216 false,
217 "info",
218 ),
219 data_row(
220 "OK",
221 "Popover elevation",
222 "shader",
223 "queued",
224 false,
225 "success",
226 ),
227 ])
228 .gap(tokens::SPACE_1)
229 .width(Size::Fill(1.0)),
230 ])]),
231 ])
232 .key("metric:table.card")
233 .width(Size::Fill(1.2))
234 .height(Size::Fill(1.0))
235}
236
237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}More examples
134fn settings_nav_item(label: &'static str, selected: bool) -> El {
135 let mut item = row([
136 El::new(Kind::Custom("nav-dot"))
137 .fill(tokens::MUTED_FOREGROUND)
138 .radius(tokens::RADIUS_PILL)
139 .width(Size::Fixed(6.0))
140 .height(Size::Fixed(6.0)),
141 text(label)
142 .font_weight(FontWeight::Medium)
143 .ellipsis()
144 .width(Size::Fill(1.0)),
145 ])
146 .key(if selected {
147 "metric:settings.nav.row".to_string()
148 } else {
149 format!("settings-nav-{label}")
150 })
151 .metrics_role(MetricsRole::ListItem)
152 .align(Align::Center)
153 .focusable();
154
155 if selected {
156 item = item.current();
157 } else {
158 item = item.color(tokens::MUTED_FOREGROUND);
159 }
160
161 item
162}
163
164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}
185
186fn profile_card() -> El {
187 card([
188 card_header([
189 card_title("Profile"),
190 card_description("This information appears in audit logs and shared documents."),
191 ]),
192 card_content([form([
193 row([
194 setting_field("Display name", "Alicia Koch", "display-name"),
195 setting_field("Email", "alicia@acme.co", "email"),
196 ])
197 .gap(tokens::SPACE_3),
198 row([
199 setting_select("Role", "Workspace admin", "role"),
200 setting_select("Region", "US East", "region"),
201 ])
202 .gap(tokens::SPACE_3),
203 ])]),
204 ])
205 .key("metric:profile.card")
206}
207
208fn setting_field(label: &'static str, value: &'static str, key: &'static str) -> El {
209 form_item([
210 form_label(label),
211 form_control(
212 text_input(value, &Selection::caret(key, value.len()), key).key(
213 if key == "display-name" {
214 "metric:form.input"
215 } else {
216 key
217 },
218 ),
219 ),
220 ])
221 .width(Size::Fill(1.0))
222}
223
224fn setting_select(label: &'static str, value: &'static str, key: &'static str) -> El {
225 form_item([form_label(label), form_control(select_trigger(key, value))]).width(Size::Fill(1.0))
226}
227
228fn preferences_card() -> El {
229 card([
230 card_header([
231 card_title("Preferences"),
232 card_description("Defaults used when creating new dashboards and exports."),
233 ]),
234 card_content([column([
235 preference_row(
236 "Compact navigation",
237 "Use tighter rows in the sidebar and command menus.",
238 switch(true).key("compact-navigation"),
239 ),
240 divider(),
241 preference_row(
242 "Email summaries",
243 "Send a daily digest when documents change.",
244 switch(false).key("email-summaries"),
245 ),
246 divider(),
247 preference_row(
248 "Require approval",
249 "Route external sharing through an owner review.",
250 checkbox(true).key("approval-required"),
251 ),
252 ])
253 .gap(0.0)
254 .width(Size::Fill(1.0))])
255 .padding(0.0),
256 ])
257 .key("metric:preferences.card")
258}
259
260fn preference_row(title: &'static str, description: &'static str, control: El) -> El {
261 row([
262 column([
263 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
264 text(description)
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0))
271 .height(Size::Hug),
272 control,
273 ])
274 .key(if title == "Compact navigation" {
275 "metric:preference.row".to_string()
276 } else {
277 format!("preference-{title}")
278 })
279 .metrics_role(MetricsRole::PreferenceRow)
280 .gap(tokens::SPACE_4)
281 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_3))
282 .align(Align::Center)
283}
284
285fn settings_aside() -> El {
286 column([security_card(), scale_card()])
287 .gap(tokens::SPACE_4)
288 .width(Size::Fixed(300.0))
289 .height(Size::Fill(1.0))
290}
291
292fn security_card() -> El {
293 card([
294 card_header([
295 card_title("Security"),
296 card_description("Two-factor authentication is enabled for all privileged users."),
297 ]),
298 card_content([
299 compact_stat("Passkeys", "2 registered", badge("On").success()),
300 compact_stat("Sessions", "3 active", button("Review").secondary()),
301 ]),
302 ])
303 .width(Size::Fill(1.0))
304}
305
306fn scale_card() -> El {
307 card([
308 card_header([
309 card_title("Interface scale"),
310 card_description("Reference captures keep browser zoom fixed and vary root UI scale."),
311 ]),
312 card_content([
313 row([text("Dense").caption(), spacer(), text("Default").caption()]),
314 slider(0.66, tokens::PRIMARY)
315 .key("interface-scale")
316 .width(Size::Fill(1.0)),
317 ]),
318 ])
319 .width(Size::Fill(1.0))
320}
321
322fn compact_stat(title: &'static str, detail: &'static str, control: El) -> El {
323 row([
324 column([
325 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
326 text(detail).caption().ellipsis().width(Size::Fill(1.0)),
327 ])
328 .gap(2.0)
329 .width(Size::Fill(1.0))
330 .height(Size::Hug),
331 control,
332 ])
333 .gap(tokens::SPACE_2)
334 .height(Size::Fixed(44.0))
335 .align(Align::Center)
336}
337
338fn section_label(label: &'static str) -> El {
339 text(label)
340 .caption()
341 .height(Size::Fixed(22.0))
342 .padding(Sides::xy(tokens::SPACE_2, 0.0))
343}
344
345fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
346 let mut item = row([
347 icon(icon_name)
348 .color(tokens::MUTED_FOREGROUND)
349 .icon_size(tokens::ICON_SM)
350 .width(Size::Fixed(tokens::ICON_SM)),
351 text(label)
352 .font_weight(FontWeight::Medium)
353 .ellipsis()
354 .width(Size::Fill(1.0)),
355 ])
356 .key(if selected {
357 "metric:sidebar.nav.row".to_string()
358 } else {
359 format!("side-item-{label}")
360 })
361 .metrics_role(MetricsRole::ListItem)
362 .gap(tokens::SPACE_2)
363 .padding(Sides::xy(tokens::SPACE_2, 0.0))
364 .height(Size::Fixed(32.0))
365 .align(Align::Center)
366 .focusable();
367
368 if selected {
369 item = item.current();
370 } else {
371 item = item.color(tokens::MUTED_FOREGROUND);
372 }
373
374 item
375}109fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
110 let mut item = row([
111 icon(icon_name)
112 .color(tokens::MUTED_FOREGROUND)
113 .icon_size(tokens::ICON_SM)
114 .width(Size::Fixed(tokens::ICON_SM)),
115 text(label)
116 .font_weight(FontWeight::Medium)
117 .ellipsis()
118 .width(Size::Fill(1.0)),
119 ])
120 .key(if selected {
121 "metric:sidebar.nav.row".to_string()
122 } else {
123 format!("side-item-{label}")
124 })
125 .metrics_role(MetricsRole::ListItem)
126 .gap(tokens::SPACE_2)
127 .padding(Sides::xy(tokens::SPACE_2, 0.0))
128 .height(Size::Fixed(32.0))
129 .align(Align::Center)
130 .focusable();
131
132 if selected {
133 item = item.current();
134 } else {
135 item = item.color(tokens::MUTED_FOREGROUND);
136 }
137
138 item
139}Sourcepub fn always_show_focus_ring(self) -> Self
pub fn always_show_focus_ring(self) -> Self
Show the focus ring on this node even when focus arrived via
pointer click. Default focus-ring behavior follows the web
platform’s :focus-visible rule — ring on Tab, no ring on
click. Widgets where the ring is meaningful regardless of
source — text input, text area — opt in here so clicking into
the field still raises the “now active” affordance. Implies
nothing about focusability; pair with .focusable().
Sourcepub fn selectable(self) -> Self
pub fn selectable(self) -> Self
Opt this node into the library’s text-selection system. The
node must also carry an explicit .key(...); selection requires
stable identity across rebuilds the same way focus does.
Sourcepub fn selection_source(self, source: SelectionSource) -> Self
pub fn selection_source(self, source: SelectionSource) -> Self
Attach source-backed copy/hit-test text for this selectable
node. The node still needs .selectable().key(...); this only
changes how selection offsets map to copied text.
Sourcepub fn capture_keys(self) -> Self
pub fn capture_keys(self) -> Self
Opt this node into raw key capture when focused. While this
node is the focused target, the library’s traversal/activation
defaults are bypassed and raw KeyDown events are delivered for
the widget to interpret. Escape is still treated as “exit
editing”: the raw KeyDown is delivered first, then focus is
cleared. Implies focusable.
Sourcepub fn alpha_follows_focused_ancestor(self) -> Self
pub fn alpha_follows_focused_ancestor(self) -> Self
Multiply this element’s paint opacity by the nearest focusable ancestor’s focus envelope.
Sourcepub fn blink_when_focused(self) -> Self
pub fn blink_when_focused(self) -> Self
Multiply this node’s paint opacity by the runtime’s caret blink alpha.
Sourcepub fn state_follows_interactive_ancestor(self) -> Self
pub fn state_follows_interactive_ancestor(self) -> Self
Borrow hover and press visual envelopes from the nearest focusable ancestor.
Sourcepub fn hover_alpha(self, rest: f32, peak: f32) -> Self
pub fn hover_alpha(self, rest: f32, peak: f32) -> Self
Bind this element’s paint opacity to the subtree interaction
envelope — the max of hover, focus, and press for the subtree
rooted at this element.
At rest (no descendant is the active hover, focus, or press
target) the element paints at rest. At full envelope it paints
at peak. Both are clamped to [0.0, 1.0], with linear
interpolation in between following the eased envelope.
“Subtree” matches CSS :hover semantics: hovering, focusing, or
pressing any descendant keeps the element revealed. A
hover-revealed close icon stays visible while the cursor moves
across the tab body or while the tab is keyboard-focused; an
action pill stays visible while the cursor moves between
focusable buttons inside it. The trigger isn’t strictly
“hover” — focus and press also count — but hover is the
dominant case and the name reflects it.
Layout-neutral — the element keeps its computed rect at all times. Use for hover-revealed close buttons, secondary actions on list rows, hover-only validation icons, and other “show on interaction” patterns where the surrounding layout shouldn’t shift.
§Beyond alpha
For the other common hover affordances — Material-style lift
(translate_y), button-pop (scale), tint shift (fill) —
drive the prop from app code using
crate::BuildCx::is_hovering_within plus
Self::animate:
fn build(&self, cx: &BuildCx) -> El {
let lifted = cx.is_hovering_within("card");
card([...])
.key("card")
.focusable()
.translate(0.0, if lifted { -2.0 } else { 0.0 })
.scale(if lifted { 1.02 } else { 1.0 })
.animate(Timing::SPRING_QUICK)
}is_hovering_within reads the same subtree predicate
hover_alpha consumes (CSS :hover-style cascade). animate
eases the prop between the two build values across frames, so
the transition is smooth without per-channel declarative API.
hover_alpha itself is the alpha-channel shorthand — it skips
the boolean-to-value conversion and the per-node animate
allocation, since alpha is the dominant hover affordance.
Sourcepub fn at(self, file: &'static str, line: u32) -> Self
pub fn at(self, file: &'static str, line: u32) -> Self
Examples found in repository?
282fn token_chip(token: TokenDef, palette: &Palette) -> El {
283 let resolved = palette.resolve(token.color);
284 row([
285 El::new(Kind::Custom("palette-swatch"))
286 .at(file!(), line!())
287 .fill(token.color)
288 .stroke(tokens::BORDER)
289 .radius(tokens::RADIUS_SM)
290 .width(Size::Fixed(42.0))
291 .height(Size::Fixed(34.0)),
292 column([
293 text(token.name)
294 .label()
295 .ellipsis()
296 .nowrap_text()
297 .width(Size::Fill(1.0)),
298 mono(rgba_label(resolved)).caption().muted(),
299 ])
300 .gap(0.0)
301 .width(Size::Fill(1.0))
302 .height(Size::Hug),
303 ])
304 .gap(tokens::SPACE_2)
305 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_2))
306 .align(Align::Center)
307 .fill(tokens::CARD)
308 .stroke(tokens::BORDER)
309 .radius(tokens::RADIUS_MD)
310 .width(Size::Fill(1.0))
311 .height(Size::Fixed(54.0))
312}Sourcepub fn at_loc(self, loc: &'static Location<'static>) -> Self
pub fn at_loc(self, loc: &'static Location<'static>) -> Self
Set source from a Location (used internally by
#[track_caller] constructors).
Sourcepub fn from_library(self) -> Self
pub fn from_library(self) -> Self
Mark this El as constructed inside an aetna library closure
where #[track_caller] doesn’t reach user code (e.g. the
.map(|item| ...) body inside tabs_list, radio_group,
etc.). The lint pass uses this flag to walk blame attribution
upward to the nearest user-source ancestor instead of pointing
findings at aetna-core internals. User code never needs to call
this.
Source§impl El
impl El
Sourcepub fn width(self, w: Size) -> Self
pub fn width(self, w: Size) -> Self
Examples found in repository?
44fn sidebar() -> El {
45 column([
46 column([h2("Aetna"), text("calibration").muted()])
47 .key("metric:sidebar.brand")
48 .gap(tokens::SPACE_1)
49 .height(Size::Hug),
50 spacer().height(Size::Fixed(tokens::SPACE_4)),
51 nav_item("01", "Overview", true),
52 nav_item("02", "Commands", false),
53 nav_item("03", "Tables", false),
54 nav_item("04", "Forms", false),
55 spacer(),
56 badge("dark theme").muted(),
57 ])
58 .gap(tokens::SPACE_2)
59 .padding(tokens::SPACE_5)
60 .key("metric:sidebar")
61 .width(Size::Fixed(220.0))
62 .height(Size::Fill(1.0))
63 .fill(tokens::CARD)
64 .stroke(tokens::BORDER)
65}
66
67fn nav_item(icon: &'static str, label: &'static str, selected: bool) -> El {
68 let mut item = row([
69 icon_cell(icon),
70 text(label)
71 .font_weight(FontWeight::Medium)
72 .ellipsis()
73 .width(Size::Fill(1.0)),
74 ])
75 .key(if selected {
76 "metric:sidebar.nav.row".to_string()
77 } else {
78 format!("nav-{label}")
79 })
80 .metrics_role(MetricsRole::ListItem)
81 .gap(tokens::SPACE_3)
82 .padding(Sides::xy(tokens::SPACE_2, 0.0))
83 .height(Size::Fixed(40.0))
84 .align(Align::Center)
85 .focusable();
86
87 if selected {
88 item = item.current();
89 }
90
91 item
92}
93
94fn main_panel() -> El {
95 column([
96 toolbar(),
97 column([
98 row([
99 kpi_card("Latency", "42 ms", "-18%", true),
100 kpi_card("Runs", "1,284", "+12%", true),
101 kpi_card("Errors", "7", "+2", false),
102 ])
103 .gap(tokens::SPACE_4),
104 row([table_card(), command_card()])
105 .gap(tokens::SPACE_4)
106 .height(Size::Fill(1.0))
107 .align(Align::Stretch),
108 ])
109 .gap(tokens::SPACE_4)
110 .height(Size::Fill(1.0))
111 .align(Align::Stretch),
112 ])
113 .padding(tokens::SPACE_7)
114 .gap(tokens::SPACE_2)
115 .width(Size::Fill(1.0))
116 .height(Size::Fill(1.0))
117}
118
119fn toolbar() -> El {
120 row([
121 column([
122 h1("Polish calibration").key("metric:page.title"),
123 text("A representative app surface for default tuning.")
124 .muted()
125 .key("metric:page.subtitle"),
126 ])
127 .gap(tokens::SPACE_2)
128 .height(Size::Hug),
129 spacer(),
130 button_with_icon("search", "Preview")
131 .secondary()
132 .key("metric:action.secondary"),
133 button_with_icon("upload", "Publish")
134 .primary()
135 .key("metric:action.primary"),
136 ])
137 .key("metric:header")
138 .gap(tokens::SPACE_4)
139 .height(Size::Hug)
140 .align(Align::Start)
141}
142
143fn kpi_card(label: &'static str, value: &'static str, delta: &'static str, positive: bool) -> El {
144 let delta_badge = if positive {
145 badge(delta).success()
146 } else {
147 badge(delta).destructive()
148 };
149 let delta_badge = if label == "Latency" {
150 delta_badge.key("metric:kpi.badge")
151 } else {
152 delta_badge
153 };
154 let value_text = h2(value).display();
155 let value_text = if label == "Latency" {
156 value_text.key("metric:kpi.value")
157 } else {
158 value_text
159 };
160 card([
161 card_header([card_title(label)]),
162 card_content([
163 row([value_text, spacer(), delta_badge]).align(Align::Center),
164 text(if positive {
165 "Moving in the expected direction"
166 } else {
167 "Needs visual attention"
168 })
169 .muted(),
170 ])
171 .gap(tokens::SPACE_6),
172 ])
173 .key(if label == "Latency" {
174 "metric:kpi.card"
175 } else {
176 label
177 })
178 .width(Size::Fill(1.0))
179}
180
181fn table_card() -> El {
182 card([
183 card_header([card_title("Reference rows")]),
184 card_content([table([
185 table_header([table_row([
186 table_head("Status").width(Size::Fixed(86.0)),
187 table_head("Surface").width(Size::Fill(1.0)),
188 table_head("Owner").width(Size::Fixed(110.0)),
189 table_head("State").width(Size::Fixed(86.0)),
190 ])
191 .key("metric:table.header")]),
192 divider(),
193 table_body([
194 data_row("OK", "Settings card", "core", "selected", true, "success"),
195 data_row(
196 "WARN",
197 "Command palette density",
198 "widgets",
199 "needs work",
200 false,
201 "warning",
202 ),
203 data_row(
204 "ERR",
205 "Disabled and invalid states",
206 "style",
207 "missing",
208 false,
209 "destructive",
210 ),
211 data_row(
212 "INFO",
213 "Token resolution",
214 "theme",
215 "planned",
216 false,
217 "info",
218 ),
219 data_row(
220 "OK",
221 "Popover elevation",
222 "shader",
223 "queued",
224 false,
225 "success",
226 ),
227 ])
228 .gap(tokens::SPACE_1)
229 .width(Size::Fill(1.0)),
230 ])]),
231 ])
232 .key("metric:table.card")
233 .width(Size::Fill(1.2))
234 .height(Size::Fill(1.0))
235}
236
237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}
287
288fn command_card() -> El {
289 card([
290 card_header([card_title("Command surface")]),
291 card_content([
292 text_input(
293 "Search commands...",
294 &Selection::default(),
295 "command-search",
296 )
297 .key("metric:command.input")
298 .width(Size::Fill(1.0)),
299 popover_panel([
300 command_row("git-branch", "New branch", "Ctrl+B").key("metric:command.row"),
301 command_row("git-commit", "Commit staged files", "Ctrl+Enter")
302 .key("command-row-commit"),
303 command_row("refresh-cw", "Refresh repository", "Ctrl+R")
304 .key("command-row-refresh"),
305 command_row("alert-circle", "Force push", "Danger").key("command-row-force"),
306 ])
307 .width(Size::Fill(1.0)),
308 scroll([form_probe()]).key("form-probe-scroll"),
309 ])
310 .height(Size::Fill(1.0)),
311 ])
312 .key("metric:command.card")
313 .width(Size::Fill(0.8))
314 .height(Size::Fill(1.0))
315}
316
317fn form_probe() -> El {
318 form([
319 form_item([
320 form_label("Valid input"),
321 form_control(
322 text_input(
323 "Valid input",
324 &Selection::caret("valid-input", 11),
325 "valid-input",
326 )
327 .key("metric:form.input"),
328 ),
329 form_description("Default field spacing and helper text."),
330 ]),
331 form_item([
332 form_label("Invalid input"),
333 form_control(
334 text_input(
335 "Invalid input",
336 &Selection::caret("invalid-input", 13),
337 "invalid-input",
338 )
339 .invalid(),
340 ),
341 form_message("This field needs attention."),
342 ]),
343 row([
344 button("Disabled").secondary().disabled(),
345 button("Loading").primary().loading(),
346 spacer(),
347 ]),
348 ])
349 .padding(tokens::SPACE_3)
350 .fill(tokens::MUTED)
351 .stroke(tokens::BORDER)
352 .radius(tokens::RADIUS_MD)
353}
354
355fn icon_cell(label: &'static str) -> El {
356 El::new(Kind::Custom("icon_cell"))
357 .style_profile(StyleProfile::Surface)
358 .text(label)
359 .text_align(TextAlign::Center)
360 .caption()
361 .font_weight(FontWeight::Semibold)
362 .fill(tokens::MUTED)
363 .stroke(tokens::BORDER)
364 .radius(tokens::RADIUS_SM)
365 .width(Size::Fixed(26.0))
366 .height(Size::Fixed(26.0))
367}More examples
44fn graph_cell(lane: u8, selected: bool) -> El {
45 let lane_color = lane_palette(lane);
46 let ring_color = if selected {
47 Color::rgb(245, 245, 250)
48 } else {
49 lane_color
50 };
51 let ring_w = if selected { 2.5 } else { 1.5 };
52 let radius = 5.0;
53 let line_w = 2.0;
54 let lane_frac = (lane as f32 + 0.5) / LANE_COUNT as f32;
55
56 El::new(Kind::Custom("graph_cell"))
57 .width(Size::Fixed(GRAPH_WIDTH))
58 .height(Size::Fixed(ROW_HEIGHT))
59 .shader(
60 ShaderBinding::custom("commit_node")
61 .color("vec_a", tokens::BACKGROUND)
62 .color("vec_b", ring_color)
63 .vec4("vec_c", [radius, ring_w, line_w, lane_frac]),
64 )
65 .fill(lane_color)
66}48fn fixture() -> El {
49 let centre = h2("Compass").center_text();
50 let dirs = [
51 ("North", "n"),
52 ("NE", "ne"),
53 ("East", "e"),
54 ("SE", "se"),
55 ("South", "s"),
56 ("SW", "sw"),
57 ("West", "w"),
58 ("NW", "nw"),
59 ];
60
61 let mut children: Vec<El> = vec![centre];
62 for (label, k) in dirs {
63 children.push(button(label).key(k).primary());
64 }
65
66 column([
67 h1("Custom layout — circular"),
68 paragraph(
69 "Eight buttons positioned on a circle by an author-supplied \
70 LayoutFn. Stock paint, automatic hover/press, and hit-test \
71 all keep working — only the rect distribution changed.",
72 )
73 .muted(),
74 stack(children)
75 .key("compass")
76 .layout(circular)
77 .width(Size::Fill(1.0))
78 .height(Size::Fixed(360.0)),
79 ])
80 .gap(tokens::SPACE_4)
81 .padding(tokens::SPACE_7)
82}31fn fixture() -> El {
32 // Apps wrap their main view in `overlays(main, [])` so the
33 // runtime can append the synthesized toast layer as an overlay
34 // sibling — same convention as for popovers and modals.
35 overlays(
36 column([
37 h2("Toasts"),
38 paragraph(
39 "Apps queue toasts by returning ToastSpec values from \
40 App::drain_toasts. The runtime stamps each with a TTL, \
41 stacks them at the bottom-right corner, and dismisses \
42 them on click or auto-expiry.",
43 )
44 .muted(),
45 row([
46 button("Save changes").key("save"),
47 button("Trigger error").key("err"),
48 button("Show info").key("info"),
49 ])
50 .gap(tokens::SPACE_2),
51 ])
52 .gap(tokens::SPACE_4)
53 .padding(tokens::SPACE_7)
54 .width(Size::Fill(1.0))
55 .height(Size::Fill(1.0)),
56 [],
57 )
58}18fn fixture() -> El {
19 column([
20 h2("Inline runs"),
21 text_runs([
22 text("Aetna's attributed-text path lets you compose runs with "),
23 text("bold").bold(),
24 text(", "),
25 text("italic").italic(),
26 text(", "),
27 text("colored").color(tokens::DESTRUCTIVE),
28 text(", and "),
29 text("inline code").code(),
30 text(" segments inside one wrapping paragraph."),
31 hard_break(),
32 text("Hard breaks act like ").muted(),
33 text("<br>").code(),
34 text(" — they end the current line without breaking out of the run.").muted(),
35 ])
36 .wrap_text()
37 .width(Size::Fill(1.0))
38 .height(Size::Hug),
39 paragraph(
40 "All of the above flows through one cosmic-text rich-text shape — \
41 wrapping decisions cross run boundaries the way real prose wraps.",
42 )
43 .muted(),
44 ])
45 .gap(tokens::SPACE_4)
46 .padding(tokens::SPACE_7)
47 .width(Size::Fixed(640.0))
48}77fn palette_demo(label: &'static str, palette: &Palette) -> El {
78 column([
79 row([
80 column([
81 h1("Palette demo"),
82 text("Aetna and Radix palettes rendered through Aetna tokens.").muted(),
83 ])
84 .gap(tokens::SPACE_2)
85 .height(Size::Hug),
86 spacer(),
87 badge(label).muted(),
88 ])
89 .align(Align::Start)
90 .height(Size::Hug),
91 row([
92 token_section(
93 "Core tokens",
94 "The shadcn-shaped semantic vocabulary.",
95 &CORE_TOKENS,
96 palette,
97 )
98 .width(Size::Fill(1.25)),
99 column([
100 token_section(
101 "Aetna extensions",
102 "Component and status tokens layered on top.",
103 &EXTENSION_TOKENS,
104 palette,
105 ),
106 component_section(),
107 ])
108 .gap(tokens::SPACE_4)
109 .width(Size::Fill(1.0))
110 .height(Size::Fill(1.0)),
111 ])
112 .gap(tokens::SPACE_4)
113 .align(Align::Stretch)
114 .height(Size::Fill(1.0)),
115 ])
116 .padding(tokens::SPACE_8)
117 .gap(tokens::SPACE_6)
118 .fill_size()
119 .fill(tokens::BACKGROUND)
120}
121
122const CORE_TOKENS: [TokenDef; 19] = [
123 TokenDef {
124 name: "background",
125 color: tokens::BACKGROUND,
126 },
127 TokenDef {
128 name: "foreground",
129 color: tokens::FOREGROUND,
130 },
131 TokenDef {
132 name: "card",
133 color: tokens::CARD,
134 },
135 TokenDef {
136 name: "card-foreground",
137 color: tokens::CARD_FOREGROUND,
138 },
139 TokenDef {
140 name: "popover",
141 color: tokens::POPOVER,
142 },
143 TokenDef {
144 name: "popover-foreground",
145 color: tokens::POPOVER_FOREGROUND,
146 },
147 TokenDef {
148 name: "primary",
149 color: tokens::PRIMARY,
150 },
151 TokenDef {
152 name: "primary-foreground",
153 color: tokens::PRIMARY_FOREGROUND,
154 },
155 TokenDef {
156 name: "secondary",
157 color: tokens::SECONDARY,
158 },
159 TokenDef {
160 name: "secondary-foreground",
161 color: tokens::SECONDARY_FOREGROUND,
162 },
163 TokenDef {
164 name: "muted",
165 color: tokens::MUTED,
166 },
167 TokenDef {
168 name: "muted-foreground",
169 color: tokens::MUTED_FOREGROUND,
170 },
171 TokenDef {
172 name: "accent",
173 color: tokens::ACCENT,
174 },
175 TokenDef {
176 name: "accent-foreground",
177 color: tokens::ACCENT_FOREGROUND,
178 },
179 TokenDef {
180 name: "destructive",
181 color: tokens::DESTRUCTIVE,
182 },
183 TokenDef {
184 name: "destructive-foreground",
185 color: tokens::DESTRUCTIVE_FOREGROUND,
186 },
187 TokenDef {
188 name: "border",
189 color: tokens::BORDER,
190 },
191 TokenDef {
192 name: "input",
193 color: tokens::INPUT,
194 },
195 TokenDef {
196 name: "ring",
197 color: tokens::RING,
198 },
199];
200
201const EXTENSION_TOKENS: [TokenDef; 12] = [
202 TokenDef {
203 name: "success",
204 color: tokens::SUCCESS,
205 },
206 TokenDef {
207 name: "success-foreground",
208 color: tokens::SUCCESS_FOREGROUND,
209 },
210 TokenDef {
211 name: "warning",
212 color: tokens::WARNING,
213 },
214 TokenDef {
215 name: "warning-foreground",
216 color: tokens::WARNING_FOREGROUND,
217 },
218 TokenDef {
219 name: "info",
220 color: tokens::INFO,
221 },
222 TokenDef {
223 name: "info-foreground",
224 color: tokens::INFO_FOREGROUND,
225 },
226 TokenDef {
227 name: "link-foreground",
228 color: tokens::LINK_FOREGROUND,
229 },
230 TokenDef {
231 name: "overlay-scrim",
232 color: tokens::OVERLAY_SCRIM,
233 },
234 TokenDef {
235 name: "scrollbar-thumb",
236 color: tokens::SCROLLBAR_THUMB_FILL,
237 },
238 TokenDef {
239 name: "scrollbar-thumb-active",
240 color: tokens::SCROLLBAR_THUMB_FILL_ACTIVE,
241 },
242 TokenDef {
243 name: "selection-bg",
244 color: tokens::SELECTION_BG,
245 },
246 TokenDef {
247 name: "selection-bg-unfocused",
248 color: tokens::SELECTION_BG_UNFOCUSED,
249 },
250];
251
252fn token_section(
253 title: &'static str,
254 description: &'static str,
255 tokens: &'static [TokenDef],
256 palette: &Palette,
257) -> El {
258 card([
259 card_header([card_title(title), card_description(description)]),
260 card_content([swatch_grid(tokens, palette)]),
261 ])
262 .height(Size::Fill(1.0))
263}
264
265fn swatch_grid(defs: &'static [TokenDef], palette: &Palette) -> El {
266 let rows = defs
267 .chunks(2)
268 .map(|chunk| {
269 row(chunk.iter().map(|token| token_chip(*token, palette)))
270 .gap(tokens::SPACE_3)
271 .align(Align::Center)
272 .width(Size::Fill(1.0))
273 })
274 .collect::<Vec<_>>();
275
276 column(rows)
277 .gap(tokens::SPACE_3)
278 .width(Size::Fill(1.0))
279 .height(Size::Hug)
280}
281
282fn token_chip(token: TokenDef, palette: &Palette) -> El {
283 let resolved = palette.resolve(token.color);
284 row([
285 El::new(Kind::Custom("palette-swatch"))
286 .at(file!(), line!())
287 .fill(token.color)
288 .stroke(tokens::BORDER)
289 .radius(tokens::RADIUS_SM)
290 .width(Size::Fixed(42.0))
291 .height(Size::Fixed(34.0)),
292 column([
293 text(token.name)
294 .label()
295 .ellipsis()
296 .nowrap_text()
297 .width(Size::Fill(1.0)),
298 mono(rgba_label(resolved)).caption().muted(),
299 ])
300 .gap(0.0)
301 .width(Size::Fill(1.0))
302 .height(Size::Hug),
303 ])
304 .gap(tokens::SPACE_2)
305 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_2))
306 .align(Align::Center)
307 .fill(tokens::CARD)
308 .stroke(tokens::BORDER)
309 .radius(tokens::RADIUS_MD)
310 .width(Size::Fill(1.0))
311 .height(Size::Fixed(54.0))
312}
313
314fn component_section() -> El {
315 card([
316 card_header([
317 card_title("Stock widgets"),
318 card_description("The same palette applied to regular component constructors."),
319 ]),
320 card_content([
321 row([
322 button("Primary").primary(),
323 button("Secondary").secondary(),
324 button("Outline").outline(),
325 button("Ghost").ghost(),
326 ])
327 .gap(tokens::SPACE_2)
328 .align(Align::Center),
329 row([
330 badge("success").success(),
331 badge("warning").warning(),
332 badge("destructive").destructive(),
333 badge("info").info(),
334 badge("muted").muted(),
335 ])
336 .gap(tokens::SPACE_2)
337 .align(Align::Center),
338 row([
339 text_input("palette search", &Selection::default(), "palette:search")
340 .width(Size::Fill(1.0)),
341 button_with_icon("settings", "Tune").secondary(),
342 ])
343 .gap(tokens::SPACE_2)
344 .align(Align::Center),
345 row([
346 surface_sample("Card", tokens::CARD),
347 surface_sample("Muted", tokens::MUTED),
348 surface_sample("Popover", tokens::POPOVER),
349 ])
350 .gap(tokens::SPACE_3)
351 .align(Align::Stretch),
352 ])
353 .gap(tokens::SPACE_4),
354 ])
355 .height(Size::Hug)
356}
357
358fn surface_sample(title: &'static str, fill: Color) -> El {
359 column([
360 text(title).label(),
361 text("surface sample").caption().muted(),
362 ])
363 .gap(tokens::SPACE_1)
364 .padding(tokens::SPACE_3)
365 .fill(fill)
366 .stroke(tokens::BORDER)
367 .radius(tokens::RADIUS_MD)
368 .width(Size::Fill(1.0))
369 .height(Size::Fixed(76.0))
370}Sourcepub fn height(self, h: Size) -> Self
pub fn height(self, h: Size) -> Self
Examples found in repository?
20fn list_rows() -> Vec<El> {
21 (0..40)
22 .map(|i| {
23 row([
24 text(format!("{i:02}.")).mono().muted(),
25 text(format!("scrollable list item {i}")),
26 ])
27 .gap(tokens::SPACE_2)
28 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_1))
29 .height(Size::Fixed(28.0))
30 .align(Align::Center)
31 })
32 .collect()
33}
34
35fn fixture() -> El {
36 column([
37 h2("Scrollbar"),
38 text("scroll() and virtual_list() show a draggable thumb by default.").muted(),
39 row([
40 // 1) scroll() — default-on scrollbar.
41 column([
42 text("scroll() — default").bold(),
43 scroll(list_rows())
44 .height(Size::Fixed(240.0))
45 .padding(tokens::SPACE_2)
46 .stroke(tokens::BORDER)
47 .stroke_width(1.0)
48 .radius(tokens::RADIUS_MD),
49 ])
50 .gap(tokens::SPACE_2)
51 .width(Size::Fill(1.0))
52 .height(Size::Hug),
53 // 2) virtual_list — thumb scales to content size.
54 column([
55 text("virtual_list(200, 28)").bold(),
56 virtual_list(200, 28.0, |i| {
57 row([
58 text(format!("{i:03}")).mono().muted(),
59 text(format!("row {i}")),
60 ])
61 .gap(tokens::SPACE_2)
62 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_1))
63 .height(Size::Fixed(28.0))
64 .align(Align::Center)
65 })
66 .height(Size::Fixed(240.0))
67 .padding(tokens::SPACE_2)
68 .stroke(tokens::BORDER)
69 .stroke_width(1.0)
70 .radius(tokens::RADIUS_MD),
71 ])
72 .gap(tokens::SPACE_2)
73 .width(Size::Fill(1.0))
74 .height(Size::Hug),
75 // 3) Opt-out: same content, no thumb.
76 column([
77 text("scroll().no_scrollbar()").bold(),
78 scroll(list_rows())
79 .no_scrollbar()
80 .height(Size::Fixed(240.0))
81 .padding(tokens::SPACE_2)
82 .stroke(tokens::BORDER)
83 .stroke_width(1.0)
84 .radius(tokens::RADIUS_MD),
85 ])
86 .gap(tokens::SPACE_2)
87 .width(Size::Fill(1.0))
88 .height(Size::Hug),
89 ])
90 .gap(tokens::SPACE_4)
91 .width(Size::Fill(1.0)),
92 ])
93 .gap(tokens::SPACE_4)
94 .padding(tokens::SPACE_7)
95}More examples
18fn build_row(i: usize) -> El {
19 let badge_el = match i % 5 {
20 0 => badge("info").muted(),
21 1 => badge("warn").warning(),
22 2 => badge("ok").success(),
23 3 => badge("err").destructive(),
24 _ => spacer(),
25 };
26 row([
27 text(format!("#{i:05}")).mono(),
28 spacer(),
29 text(format!("entry {i}")),
30 spacer(),
31 badge_el,
32 ])
33 .key(format!("row-{i}"))
34 .gap(tokens::SPACE_3)
35 .padding(Sides::xy(tokens::SPACE_3, tokens::SPACE_2))
36 .height(Size::Fixed(ROW_HEIGHT))
37}
38
39fn fixture() -> El {
40 column([
41 h1("Virtualized list"),
42 paragraph(format!(
43 "{ROW_COUNT} rows × {ROW_HEIGHT}px in a windowed viewport. \
44 Only the rows intersecting the viewport are realized — see \
45 `tree.txt` for proof."
46 ))
47 .muted(),
48 virtual_list(ROW_COUNT, ROW_HEIGHT, build_row)
49 .key("entries")
50 .height(Size::Fill(1.0)),
51 ])
52 .gap(tokens::SPACE_4)
53 .padding(tokens::SPACE_7)
54}44fn sidebar() -> El {
45 column([
46 column([h2("Aetna"), text("calibration").muted()])
47 .key("metric:sidebar.brand")
48 .gap(tokens::SPACE_1)
49 .height(Size::Hug),
50 spacer().height(Size::Fixed(tokens::SPACE_4)),
51 nav_item("01", "Overview", true),
52 nav_item("02", "Commands", false),
53 nav_item("03", "Tables", false),
54 nav_item("04", "Forms", false),
55 spacer(),
56 badge("dark theme").muted(),
57 ])
58 .gap(tokens::SPACE_2)
59 .padding(tokens::SPACE_5)
60 .key("metric:sidebar")
61 .width(Size::Fixed(220.0))
62 .height(Size::Fill(1.0))
63 .fill(tokens::CARD)
64 .stroke(tokens::BORDER)
65}
66
67fn nav_item(icon: &'static str, label: &'static str, selected: bool) -> El {
68 let mut item = row([
69 icon_cell(icon),
70 text(label)
71 .font_weight(FontWeight::Medium)
72 .ellipsis()
73 .width(Size::Fill(1.0)),
74 ])
75 .key(if selected {
76 "metric:sidebar.nav.row".to_string()
77 } else {
78 format!("nav-{label}")
79 })
80 .metrics_role(MetricsRole::ListItem)
81 .gap(tokens::SPACE_3)
82 .padding(Sides::xy(tokens::SPACE_2, 0.0))
83 .height(Size::Fixed(40.0))
84 .align(Align::Center)
85 .focusable();
86
87 if selected {
88 item = item.current();
89 }
90
91 item
92}
93
94fn main_panel() -> El {
95 column([
96 toolbar(),
97 column([
98 row([
99 kpi_card("Latency", "42 ms", "-18%", true),
100 kpi_card("Runs", "1,284", "+12%", true),
101 kpi_card("Errors", "7", "+2", false),
102 ])
103 .gap(tokens::SPACE_4),
104 row([table_card(), command_card()])
105 .gap(tokens::SPACE_4)
106 .height(Size::Fill(1.0))
107 .align(Align::Stretch),
108 ])
109 .gap(tokens::SPACE_4)
110 .height(Size::Fill(1.0))
111 .align(Align::Stretch),
112 ])
113 .padding(tokens::SPACE_7)
114 .gap(tokens::SPACE_2)
115 .width(Size::Fill(1.0))
116 .height(Size::Fill(1.0))
117}
118
119fn toolbar() -> El {
120 row([
121 column([
122 h1("Polish calibration").key("metric:page.title"),
123 text("A representative app surface for default tuning.")
124 .muted()
125 .key("metric:page.subtitle"),
126 ])
127 .gap(tokens::SPACE_2)
128 .height(Size::Hug),
129 spacer(),
130 button_with_icon("search", "Preview")
131 .secondary()
132 .key("metric:action.secondary"),
133 button_with_icon("upload", "Publish")
134 .primary()
135 .key("metric:action.primary"),
136 ])
137 .key("metric:header")
138 .gap(tokens::SPACE_4)
139 .height(Size::Hug)
140 .align(Align::Start)
141}
142
143fn kpi_card(label: &'static str, value: &'static str, delta: &'static str, positive: bool) -> El {
144 let delta_badge = if positive {
145 badge(delta).success()
146 } else {
147 badge(delta).destructive()
148 };
149 let delta_badge = if label == "Latency" {
150 delta_badge.key("metric:kpi.badge")
151 } else {
152 delta_badge
153 };
154 let value_text = h2(value).display();
155 let value_text = if label == "Latency" {
156 value_text.key("metric:kpi.value")
157 } else {
158 value_text
159 };
160 card([
161 card_header([card_title(label)]),
162 card_content([
163 row([value_text, spacer(), delta_badge]).align(Align::Center),
164 text(if positive {
165 "Moving in the expected direction"
166 } else {
167 "Needs visual attention"
168 })
169 .muted(),
170 ])
171 .gap(tokens::SPACE_6),
172 ])
173 .key(if label == "Latency" {
174 "metric:kpi.card"
175 } else {
176 label
177 })
178 .width(Size::Fill(1.0))
179}
180
181fn table_card() -> El {
182 card([
183 card_header([card_title("Reference rows")]),
184 card_content([table([
185 table_header([table_row([
186 table_head("Status").width(Size::Fixed(86.0)),
187 table_head("Surface").width(Size::Fill(1.0)),
188 table_head("Owner").width(Size::Fixed(110.0)),
189 table_head("State").width(Size::Fixed(86.0)),
190 ])
191 .key("metric:table.header")]),
192 divider(),
193 table_body([
194 data_row("OK", "Settings card", "core", "selected", true, "success"),
195 data_row(
196 "WARN",
197 "Command palette density",
198 "widgets",
199 "needs work",
200 false,
201 "warning",
202 ),
203 data_row(
204 "ERR",
205 "Disabled and invalid states",
206 "style",
207 "missing",
208 false,
209 "destructive",
210 ),
211 data_row(
212 "INFO",
213 "Token resolution",
214 "theme",
215 "planned",
216 false,
217 "info",
218 ),
219 data_row(
220 "OK",
221 "Popover elevation",
222 "shader",
223 "queued",
224 false,
225 "success",
226 ),
227 ])
228 .gap(tokens::SPACE_1)
229 .width(Size::Fill(1.0)),
230 ])]),
231 ])
232 .key("metric:table.card")
233 .width(Size::Fill(1.2))
234 .height(Size::Fill(1.0))
235}
236
237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}
287
288fn command_card() -> El {
289 card([
290 card_header([card_title("Command surface")]),
291 card_content([
292 text_input(
293 "Search commands...",
294 &Selection::default(),
295 "command-search",
296 )
297 .key("metric:command.input")
298 .width(Size::Fill(1.0)),
299 popover_panel([
300 command_row("git-branch", "New branch", "Ctrl+B").key("metric:command.row"),
301 command_row("git-commit", "Commit staged files", "Ctrl+Enter")
302 .key("command-row-commit"),
303 command_row("refresh-cw", "Refresh repository", "Ctrl+R")
304 .key("command-row-refresh"),
305 command_row("alert-circle", "Force push", "Danger").key("command-row-force"),
306 ])
307 .width(Size::Fill(1.0)),
308 scroll([form_probe()]).key("form-probe-scroll"),
309 ])
310 .height(Size::Fill(1.0)),
311 ])
312 .key("metric:command.card")
313 .width(Size::Fill(0.8))
314 .height(Size::Fill(1.0))
315}
316
317fn form_probe() -> El {
318 form([
319 form_item([
320 form_label("Valid input"),
321 form_control(
322 text_input(
323 "Valid input",
324 &Selection::caret("valid-input", 11),
325 "valid-input",
326 )
327 .key("metric:form.input"),
328 ),
329 form_description("Default field spacing and helper text."),
330 ]),
331 form_item([
332 form_label("Invalid input"),
333 form_control(
334 text_input(
335 "Invalid input",
336 &Selection::caret("invalid-input", 13),
337 "invalid-input",
338 )
339 .invalid(),
340 ),
341 form_message("This field needs attention."),
342 ]),
343 row([
344 button("Disabled").secondary().disabled(),
345 button("Loading").primary().loading(),
346 spacer(),
347 ]),
348 ])
349 .padding(tokens::SPACE_3)
350 .fill(tokens::MUTED)
351 .stroke(tokens::BORDER)
352 .radius(tokens::RADIUS_MD)
353}
354
355fn icon_cell(label: &'static str) -> El {
356 El::new(Kind::Custom("icon_cell"))
357 .style_profile(StyleProfile::Surface)
358 .text(label)
359 .text_align(TextAlign::Center)
360 .caption()
361 .font_weight(FontWeight::Semibold)
362 .fill(tokens::MUTED)
363 .stroke(tokens::BORDER)
364 .radius(tokens::RADIUS_SM)
365 .width(Size::Fixed(26.0))
366 .height(Size::Fixed(26.0))
367}44fn graph_cell(lane: u8, selected: bool) -> El {
45 let lane_color = lane_palette(lane);
46 let ring_color = if selected {
47 Color::rgb(245, 245, 250)
48 } else {
49 lane_color
50 };
51 let ring_w = if selected { 2.5 } else { 1.5 };
52 let radius = 5.0;
53 let line_w = 2.0;
54 let lane_frac = (lane as f32 + 0.5) / LANE_COUNT as f32;
55
56 El::new(Kind::Custom("graph_cell"))
57 .width(Size::Fixed(GRAPH_WIDTH))
58 .height(Size::Fixed(ROW_HEIGHT))
59 .shader(
60 ShaderBinding::custom("commit_node")
61 .color("vec_a", tokens::BACKGROUND)
62 .color("vec_b", ring_color)
63 .vec4("vec_c", [radius, ring_w, line_w, lane_frac]),
64 )
65 .fill(lane_color)
66}
67
68fn build_row(c: &FakeCommit, idx: usize, selected: bool) -> El {
69 row([
70 graph_cell(c.lane, selected),
71 text(c.sha).mono().muted(),
72 text(c.subject),
73 spacer(),
74 text(format!("{} · {}", c.author, c.when)).muted(),
75 ])
76 .key(format!("commit-{idx}"))
77 .gap(tokens::SPACE_3)
78 .padding(Sides::xy(tokens::SPACE_2, 0.0))
79 .height(Size::Fixed(ROW_HEIGHT))
80 .align(Align::Center)
81}15fn scroll_list_fixture() -> El {
16 let rows: Vec<El> = (0..20)
17 .map(|i| {
18 row([
19 badge(format!("#{i}")).info(),
20 text(format!("Notification {i}")).bold(),
21 spacer(),
22 text(format!("{}m ago", i + 1)).muted(),
23 ])
24 .gap(tokens::SPACE_2)
25 .height(Size::Fixed(44.0))
26 .padding(Sides::xy(tokens::SPACE_3, tokens::SPACE_2))
27 })
28 .collect();
29
30 let list = scroll(rows)
31 .key("notifications")
32 .height(Size::Fixed(420.0))
33 .padding(tokens::SPACE_2);
34
35 column([
36 h2("Notifications"),
37 text("Roll the wheel inside the panel to scroll. The content is taller than the viewport.")
38 .muted(),
39 list,
40 ])
41 .gap(tokens::SPACE_4)
42 .padding(tokens::SPACE_7)
43}48fn fixture() -> El {
49 let centre = h2("Compass").center_text();
50 let dirs = [
51 ("North", "n"),
52 ("NE", "ne"),
53 ("East", "e"),
54 ("SE", "se"),
55 ("South", "s"),
56 ("SW", "sw"),
57 ("West", "w"),
58 ("NW", "nw"),
59 ];
60
61 let mut children: Vec<El> = vec![centre];
62 for (label, k) in dirs {
63 children.push(button(label).key(k).primary());
64 }
65
66 column([
67 h1("Custom layout — circular"),
68 paragraph(
69 "Eight buttons positioned on a circle by an author-supplied \
70 LayoutFn. Stock paint, automatic hover/press, and hit-test \
71 all keep working — only the rect distribution changed.",
72 )
73 .muted(),
74 stack(children)
75 .key("compass")
76 .layout(circular)
77 .width(Size::Fill(1.0))
78 .height(Size::Fixed(360.0)),
79 ])
80 .gap(tokens::SPACE_4)
81 .padding(tokens::SPACE_7)
82}pub fn hug(self) -> Self
Sourcepub fn fill_size(self) -> Self
pub fn fill_size(self) -> Self
Examples found in repository?
More examples
77fn palette_demo(label: &'static str, palette: &Palette) -> El {
78 column([
79 row([
80 column([
81 h1("Palette demo"),
82 text("Aetna and Radix palettes rendered through Aetna tokens.").muted(),
83 ])
84 .gap(tokens::SPACE_2)
85 .height(Size::Hug),
86 spacer(),
87 badge(label).muted(),
88 ])
89 .align(Align::Start)
90 .height(Size::Hug),
91 row([
92 token_section(
93 "Core tokens",
94 "The shadcn-shaped semantic vocabulary.",
95 &CORE_TOKENS,
96 palette,
97 )
98 .width(Size::Fill(1.25)),
99 column([
100 token_section(
101 "Aetna extensions",
102 "Component and status tokens layered on top.",
103 &EXTENSION_TOKENS,
104 palette,
105 ),
106 component_section(),
107 ])
108 .gap(tokens::SPACE_4)
109 .width(Size::Fill(1.0))
110 .height(Size::Fill(1.0)),
111 ])
112 .gap(tokens::SPACE_4)
113 .align(Align::Stretch)
114 .height(Size::Fill(1.0)),
115 ])
116 .padding(tokens::SPACE_8)
117 .gap(tokens::SPACE_6)
118 .fill_size()
119 .fill(tokens::BACKGROUND)
120}Sourcepub fn size(self, size: ComponentSize) -> Self
pub fn size(self, size: ComponentSize) -> Self
Set the t-shirt size for stock controls.
pub fn medium(self) -> Self
pub fn large(self) -> Self
Sourcepub fn metrics_role(self, role: MetricsRole) -> Self
pub fn metrics_role(self, role: MetricsRole) -> Self
Set the theme-facing stock metrics role for this widget.
Examples found in repository?
67fn nav_item(icon: &'static str, label: &'static str, selected: bool) -> El {
68 let mut item = row([
69 icon_cell(icon),
70 text(label)
71 .font_weight(FontWeight::Medium)
72 .ellipsis()
73 .width(Size::Fill(1.0)),
74 ])
75 .key(if selected {
76 "metric:sidebar.nav.row".to_string()
77 } else {
78 format!("nav-{label}")
79 })
80 .metrics_role(MetricsRole::ListItem)
81 .gap(tokens::SPACE_3)
82 .padding(Sides::xy(tokens::SPACE_2, 0.0))
83 .height(Size::Fixed(40.0))
84 .align(Align::Center)
85 .focusable();
86
87 if selected {
88 item = item.current();
89 }
90
91 item
92}More examples
134fn settings_nav_item(label: &'static str, selected: bool) -> El {
135 let mut item = row([
136 El::new(Kind::Custom("nav-dot"))
137 .fill(tokens::MUTED_FOREGROUND)
138 .radius(tokens::RADIUS_PILL)
139 .width(Size::Fixed(6.0))
140 .height(Size::Fixed(6.0)),
141 text(label)
142 .font_weight(FontWeight::Medium)
143 .ellipsis()
144 .width(Size::Fill(1.0)),
145 ])
146 .key(if selected {
147 "metric:settings.nav.row".to_string()
148 } else {
149 format!("settings-nav-{label}")
150 })
151 .metrics_role(MetricsRole::ListItem)
152 .align(Align::Center)
153 .focusable();
154
155 if selected {
156 item = item.current();
157 } else {
158 item = item.color(tokens::MUTED_FOREGROUND);
159 }
160
161 item
162}
163
164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}
185
186fn profile_card() -> El {
187 card([
188 card_header([
189 card_title("Profile"),
190 card_description("This information appears in audit logs and shared documents."),
191 ]),
192 card_content([form([
193 row([
194 setting_field("Display name", "Alicia Koch", "display-name"),
195 setting_field("Email", "alicia@acme.co", "email"),
196 ])
197 .gap(tokens::SPACE_3),
198 row([
199 setting_select("Role", "Workspace admin", "role"),
200 setting_select("Region", "US East", "region"),
201 ])
202 .gap(tokens::SPACE_3),
203 ])]),
204 ])
205 .key("metric:profile.card")
206}
207
208fn setting_field(label: &'static str, value: &'static str, key: &'static str) -> El {
209 form_item([
210 form_label(label),
211 form_control(
212 text_input(value, &Selection::caret(key, value.len()), key).key(
213 if key == "display-name" {
214 "metric:form.input"
215 } else {
216 key
217 },
218 ),
219 ),
220 ])
221 .width(Size::Fill(1.0))
222}
223
224fn setting_select(label: &'static str, value: &'static str, key: &'static str) -> El {
225 form_item([form_label(label), form_control(select_trigger(key, value))]).width(Size::Fill(1.0))
226}
227
228fn preferences_card() -> El {
229 card([
230 card_header([
231 card_title("Preferences"),
232 card_description("Defaults used when creating new dashboards and exports."),
233 ]),
234 card_content([column([
235 preference_row(
236 "Compact navigation",
237 "Use tighter rows in the sidebar and command menus.",
238 switch(true).key("compact-navigation"),
239 ),
240 divider(),
241 preference_row(
242 "Email summaries",
243 "Send a daily digest when documents change.",
244 switch(false).key("email-summaries"),
245 ),
246 divider(),
247 preference_row(
248 "Require approval",
249 "Route external sharing through an owner review.",
250 checkbox(true).key("approval-required"),
251 ),
252 ])
253 .gap(0.0)
254 .width(Size::Fill(1.0))])
255 .padding(0.0),
256 ])
257 .key("metric:preferences.card")
258}
259
260fn preference_row(title: &'static str, description: &'static str, control: El) -> El {
261 row([
262 column([
263 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
264 text(description)
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0))
271 .height(Size::Hug),
272 control,
273 ])
274 .key(if title == "Compact navigation" {
275 "metric:preference.row".to_string()
276 } else {
277 format!("preference-{title}")
278 })
279 .metrics_role(MetricsRole::PreferenceRow)
280 .gap(tokens::SPACE_4)
281 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_3))
282 .align(Align::Center)
283}
284
285fn settings_aside() -> El {
286 column([security_card(), scale_card()])
287 .gap(tokens::SPACE_4)
288 .width(Size::Fixed(300.0))
289 .height(Size::Fill(1.0))
290}
291
292fn security_card() -> El {
293 card([
294 card_header([
295 card_title("Security"),
296 card_description("Two-factor authentication is enabled for all privileged users."),
297 ]),
298 card_content([
299 compact_stat("Passkeys", "2 registered", badge("On").success()),
300 compact_stat("Sessions", "3 active", button("Review").secondary()),
301 ]),
302 ])
303 .width(Size::Fill(1.0))
304}
305
306fn scale_card() -> El {
307 card([
308 card_header([
309 card_title("Interface scale"),
310 card_description("Reference captures keep browser zoom fixed and vary root UI scale."),
311 ]),
312 card_content([
313 row([text("Dense").caption(), spacer(), text("Default").caption()]),
314 slider(0.66, tokens::PRIMARY)
315 .key("interface-scale")
316 .width(Size::Fill(1.0)),
317 ]),
318 ])
319 .width(Size::Fill(1.0))
320}
321
322fn compact_stat(title: &'static str, detail: &'static str, control: El) -> El {
323 row([
324 column([
325 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
326 text(detail).caption().ellipsis().width(Size::Fill(1.0)),
327 ])
328 .gap(2.0)
329 .width(Size::Fill(1.0))
330 .height(Size::Hug),
331 control,
332 ])
333 .gap(tokens::SPACE_2)
334 .height(Size::Fixed(44.0))
335 .align(Align::Center)
336}
337
338fn section_label(label: &'static str) -> El {
339 text(label)
340 .caption()
341 .height(Size::Fixed(22.0))
342 .padding(Sides::xy(tokens::SPACE_2, 0.0))
343}
344
345fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
346 let mut item = row([
347 icon(icon_name)
348 .color(tokens::MUTED_FOREGROUND)
349 .icon_size(tokens::ICON_SM)
350 .width(Size::Fixed(tokens::ICON_SM)),
351 text(label)
352 .font_weight(FontWeight::Medium)
353 .ellipsis()
354 .width(Size::Fill(1.0)),
355 ])
356 .key(if selected {
357 "metric:sidebar.nav.row".to_string()
358 } else {
359 format!("side-item-{label}")
360 })
361 .metrics_role(MetricsRole::ListItem)
362 .gap(tokens::SPACE_2)
363 .padding(Sides::xy(tokens::SPACE_2, 0.0))
364 .height(Size::Fixed(32.0))
365 .align(Align::Center)
366 .focusable();
367
368 if selected {
369 item = item.current();
370 } else {
371 item = item.color(tokens::MUTED_FOREGROUND);
372 }
373
374 item
375}109fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
110 let mut item = row([
111 icon(icon_name)
112 .color(tokens::MUTED_FOREGROUND)
113 .icon_size(tokens::ICON_SM)
114 .width(Size::Fixed(tokens::ICON_SM)),
115 text(label)
116 .font_weight(FontWeight::Medium)
117 .ellipsis()
118 .width(Size::Fill(1.0)),
119 ])
120 .key(if selected {
121 "metric:sidebar.nav.row".to_string()
122 } else {
123 format!("side-item-{label}")
124 })
125 .metrics_role(MetricsRole::ListItem)
126 .gap(tokens::SPACE_2)
127 .padding(Sides::xy(tokens::SPACE_2, 0.0))
128 .height(Size::Fixed(32.0))
129 .align(Align::Center)
130 .focusable();
131
132 if selected {
133 item = item.current();
134 } else {
135 item = item.color(tokens::MUTED_FOREGROUND);
136 }
137
138 item
139}Sourcepub fn padding(self, p: impl Into<Sides>) -> Self
pub fn padding(self, p: impl Into<Sides>) -> Self
Examples found in repository?
20fn list_rows() -> Vec<El> {
21 (0..40)
22 .map(|i| {
23 row([
24 text(format!("{i:02}.")).mono().muted(),
25 text(format!("scrollable list item {i}")),
26 ])
27 .gap(tokens::SPACE_2)
28 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_1))
29 .height(Size::Fixed(28.0))
30 .align(Align::Center)
31 })
32 .collect()
33}
34
35fn fixture() -> El {
36 column([
37 h2("Scrollbar"),
38 text("scroll() and virtual_list() show a draggable thumb by default.").muted(),
39 row([
40 // 1) scroll() — default-on scrollbar.
41 column([
42 text("scroll() — default").bold(),
43 scroll(list_rows())
44 .height(Size::Fixed(240.0))
45 .padding(tokens::SPACE_2)
46 .stroke(tokens::BORDER)
47 .stroke_width(1.0)
48 .radius(tokens::RADIUS_MD),
49 ])
50 .gap(tokens::SPACE_2)
51 .width(Size::Fill(1.0))
52 .height(Size::Hug),
53 // 2) virtual_list — thumb scales to content size.
54 column([
55 text("virtual_list(200, 28)").bold(),
56 virtual_list(200, 28.0, |i| {
57 row([
58 text(format!("{i:03}")).mono().muted(),
59 text(format!("row {i}")),
60 ])
61 .gap(tokens::SPACE_2)
62 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_1))
63 .height(Size::Fixed(28.0))
64 .align(Align::Center)
65 })
66 .height(Size::Fixed(240.0))
67 .padding(tokens::SPACE_2)
68 .stroke(tokens::BORDER)
69 .stroke_width(1.0)
70 .radius(tokens::RADIUS_MD),
71 ])
72 .gap(tokens::SPACE_2)
73 .width(Size::Fill(1.0))
74 .height(Size::Hug),
75 // 3) Opt-out: same content, no thumb.
76 column([
77 text("scroll().no_scrollbar()").bold(),
78 scroll(list_rows())
79 .no_scrollbar()
80 .height(Size::Fixed(240.0))
81 .padding(tokens::SPACE_2)
82 .stroke(tokens::BORDER)
83 .stroke_width(1.0)
84 .radius(tokens::RADIUS_MD),
85 ])
86 .gap(tokens::SPACE_2)
87 .width(Size::Fill(1.0))
88 .height(Size::Hug),
89 ])
90 .gap(tokens::SPACE_4)
91 .width(Size::Fill(1.0)),
92 ])
93 .gap(tokens::SPACE_4)
94 .padding(tokens::SPACE_7)
95}More examples
68fn build_row(c: &FakeCommit, idx: usize, selected: bool) -> El {
69 row([
70 graph_cell(c.lane, selected),
71 text(c.sha).mono().muted(),
72 text(c.subject),
73 spacer(),
74 text(format!("{} · {}", c.author, c.when)).muted(),
75 ])
76 .key(format!("commit-{idx}"))
77 .gap(tokens::SPACE_3)
78 .padding(Sides::xy(tokens::SPACE_2, 0.0))
79 .height(Size::Fixed(ROW_HEIGHT))
80 .align(Align::Center)
81}
82
83fn fixture() -> El {
84 #[rustfmt::skip]
85 let commits = [
86 FakeCommit { sha: "8a3f1c9", subject: "fix race condition in scheduler", author: "ada", when: "12m", lane: 0 },
87 FakeCommit { sha: "1b07d4e", subject: "tweak token tooltip wording", author: "linus", when: "1h", lane: 0 },
88 FakeCommit { sha: "9f2e4a1", subject: "wire avatar fallback identicon", author: "joelle", when: "3h", lane: 1 },
89 FakeCommit { sha: "44ab8d2", subject: "diff: word-level highlight pass", author: "raphael", when: "5h", lane: 1 },
90 FakeCommit { sha: "61c0fe7", subject: "ci: bump rust toolchain to 1.85", author: "mei", when: "7h", lane: 2 },
91 FakeCommit { sha: "a90215b", subject: "switch logging to env_logger", author: "isabel", when: "1d", lane: 2 },
92 FakeCommit { sha: "0d7e3c4", subject: "drop unused commit_detail cache", author: "noor", when: "1d", lane: 1 },
93 FakeCommit { sha: "33b2118", subject: "context-menu spacing pass", author: "kira", when: "2d", lane: 3 },
94 ];
95 let selected_idx = 3;
96 let rows = commits
97 .iter()
98 .enumerate()
99 .map(|(i, c)| build_row(c, i, i == selected_idx))
100 .collect::<Vec<_>>();
101
102 column([
103 h2("Custom-painted commit graph"),
104 text("8 commits · custom shader paints lane line + circle node").muted(),
105 column(rows).gap(0.0),
106 ])
107 .padding(tokens::SPACE_4)
108 .gap(tokens::SPACE_2)
109}18fn build_row(i: usize) -> El {
19 let badge_el = match i % 5 {
20 0 => badge("info").muted(),
21 1 => badge("warn").warning(),
22 2 => badge("ok").success(),
23 3 => badge("err").destructive(),
24 _ => spacer(),
25 };
26 row([
27 text(format!("#{i:05}")).mono(),
28 spacer(),
29 text(format!("entry {i}")),
30 spacer(),
31 badge_el,
32 ])
33 .key(format!("row-{i}"))
34 .gap(tokens::SPACE_3)
35 .padding(Sides::xy(tokens::SPACE_3, tokens::SPACE_2))
36 .height(Size::Fixed(ROW_HEIGHT))
37}
38
39fn fixture() -> El {
40 column([
41 h1("Virtualized list"),
42 paragraph(format!(
43 "{ROW_COUNT} rows × {ROW_HEIGHT}px in a windowed viewport. \
44 Only the rows intersecting the viewport are realized — see \
45 `tree.txt` for proof."
46 ))
47 .muted(),
48 virtual_list(ROW_COUNT, ROW_HEIGHT, build_row)
49 .key("entries")
50 .height(Size::Fill(1.0)),
51 ])
52 .gap(tokens::SPACE_4)
53 .padding(tokens::SPACE_7)
54}44fn sidebar() -> El {
45 column([
46 column([h2("Aetna"), text("calibration").muted()])
47 .key("metric:sidebar.brand")
48 .gap(tokens::SPACE_1)
49 .height(Size::Hug),
50 spacer().height(Size::Fixed(tokens::SPACE_4)),
51 nav_item("01", "Overview", true),
52 nav_item("02", "Commands", false),
53 nav_item("03", "Tables", false),
54 nav_item("04", "Forms", false),
55 spacer(),
56 badge("dark theme").muted(),
57 ])
58 .gap(tokens::SPACE_2)
59 .padding(tokens::SPACE_5)
60 .key("metric:sidebar")
61 .width(Size::Fixed(220.0))
62 .height(Size::Fill(1.0))
63 .fill(tokens::CARD)
64 .stroke(tokens::BORDER)
65}
66
67fn nav_item(icon: &'static str, label: &'static str, selected: bool) -> El {
68 let mut item = row([
69 icon_cell(icon),
70 text(label)
71 .font_weight(FontWeight::Medium)
72 .ellipsis()
73 .width(Size::Fill(1.0)),
74 ])
75 .key(if selected {
76 "metric:sidebar.nav.row".to_string()
77 } else {
78 format!("nav-{label}")
79 })
80 .metrics_role(MetricsRole::ListItem)
81 .gap(tokens::SPACE_3)
82 .padding(Sides::xy(tokens::SPACE_2, 0.0))
83 .height(Size::Fixed(40.0))
84 .align(Align::Center)
85 .focusable();
86
87 if selected {
88 item = item.current();
89 }
90
91 item
92}
93
94fn main_panel() -> El {
95 column([
96 toolbar(),
97 column([
98 row([
99 kpi_card("Latency", "42 ms", "-18%", true),
100 kpi_card("Runs", "1,284", "+12%", true),
101 kpi_card("Errors", "7", "+2", false),
102 ])
103 .gap(tokens::SPACE_4),
104 row([table_card(), command_card()])
105 .gap(tokens::SPACE_4)
106 .height(Size::Fill(1.0))
107 .align(Align::Stretch),
108 ])
109 .gap(tokens::SPACE_4)
110 .height(Size::Fill(1.0))
111 .align(Align::Stretch),
112 ])
113 .padding(tokens::SPACE_7)
114 .gap(tokens::SPACE_2)
115 .width(Size::Fill(1.0))
116 .height(Size::Fill(1.0))
117}
118
119fn toolbar() -> El {
120 row([
121 column([
122 h1("Polish calibration").key("metric:page.title"),
123 text("A representative app surface for default tuning.")
124 .muted()
125 .key("metric:page.subtitle"),
126 ])
127 .gap(tokens::SPACE_2)
128 .height(Size::Hug),
129 spacer(),
130 button_with_icon("search", "Preview")
131 .secondary()
132 .key("metric:action.secondary"),
133 button_with_icon("upload", "Publish")
134 .primary()
135 .key("metric:action.primary"),
136 ])
137 .key("metric:header")
138 .gap(tokens::SPACE_4)
139 .height(Size::Hug)
140 .align(Align::Start)
141}
142
143fn kpi_card(label: &'static str, value: &'static str, delta: &'static str, positive: bool) -> El {
144 let delta_badge = if positive {
145 badge(delta).success()
146 } else {
147 badge(delta).destructive()
148 };
149 let delta_badge = if label == "Latency" {
150 delta_badge.key("metric:kpi.badge")
151 } else {
152 delta_badge
153 };
154 let value_text = h2(value).display();
155 let value_text = if label == "Latency" {
156 value_text.key("metric:kpi.value")
157 } else {
158 value_text
159 };
160 card([
161 card_header([card_title(label)]),
162 card_content([
163 row([value_text, spacer(), delta_badge]).align(Align::Center),
164 text(if positive {
165 "Moving in the expected direction"
166 } else {
167 "Needs visual attention"
168 })
169 .muted(),
170 ])
171 .gap(tokens::SPACE_6),
172 ])
173 .key(if label == "Latency" {
174 "metric:kpi.card"
175 } else {
176 label
177 })
178 .width(Size::Fill(1.0))
179}
180
181fn table_card() -> El {
182 card([
183 card_header([card_title("Reference rows")]),
184 card_content([table([
185 table_header([table_row([
186 table_head("Status").width(Size::Fixed(86.0)),
187 table_head("Surface").width(Size::Fill(1.0)),
188 table_head("Owner").width(Size::Fixed(110.0)),
189 table_head("State").width(Size::Fixed(86.0)),
190 ])
191 .key("metric:table.header")]),
192 divider(),
193 table_body([
194 data_row("OK", "Settings card", "core", "selected", true, "success"),
195 data_row(
196 "WARN",
197 "Command palette density",
198 "widgets",
199 "needs work",
200 false,
201 "warning",
202 ),
203 data_row(
204 "ERR",
205 "Disabled and invalid states",
206 "style",
207 "missing",
208 false,
209 "destructive",
210 ),
211 data_row(
212 "INFO",
213 "Token resolution",
214 "theme",
215 "planned",
216 false,
217 "info",
218 ),
219 data_row(
220 "OK",
221 "Popover elevation",
222 "shader",
223 "queued",
224 false,
225 "success",
226 ),
227 ])
228 .gap(tokens::SPACE_1)
229 .width(Size::Fill(1.0)),
230 ])]),
231 ])
232 .key("metric:table.card")
233 .width(Size::Fill(1.2))
234 .height(Size::Fill(1.0))
235}
236
237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}
287
288fn command_card() -> El {
289 card([
290 card_header([card_title("Command surface")]),
291 card_content([
292 text_input(
293 "Search commands...",
294 &Selection::default(),
295 "command-search",
296 )
297 .key("metric:command.input")
298 .width(Size::Fill(1.0)),
299 popover_panel([
300 command_row("git-branch", "New branch", "Ctrl+B").key("metric:command.row"),
301 command_row("git-commit", "Commit staged files", "Ctrl+Enter")
302 .key("command-row-commit"),
303 command_row("refresh-cw", "Refresh repository", "Ctrl+R")
304 .key("command-row-refresh"),
305 command_row("alert-circle", "Force push", "Danger").key("command-row-force"),
306 ])
307 .width(Size::Fill(1.0)),
308 scroll([form_probe()]).key("form-probe-scroll"),
309 ])
310 .height(Size::Fill(1.0)),
311 ])
312 .key("metric:command.card")
313 .width(Size::Fill(0.8))
314 .height(Size::Fill(1.0))
315}
316
317fn form_probe() -> El {
318 form([
319 form_item([
320 form_label("Valid input"),
321 form_control(
322 text_input(
323 "Valid input",
324 &Selection::caret("valid-input", 11),
325 "valid-input",
326 )
327 .key("metric:form.input"),
328 ),
329 form_description("Default field spacing and helper text."),
330 ]),
331 form_item([
332 form_label("Invalid input"),
333 form_control(
334 text_input(
335 "Invalid input",
336 &Selection::caret("invalid-input", 13),
337 "invalid-input",
338 )
339 .invalid(),
340 ),
341 form_message("This field needs attention."),
342 ]),
343 row([
344 button("Disabled").secondary().disabled(),
345 button("Loading").primary().loading(),
346 spacer(),
347 ]),
348 ])
349 .padding(tokens::SPACE_3)
350 .fill(tokens::MUTED)
351 .stroke(tokens::BORDER)
352 .radius(tokens::RADIUS_MD)
353}15fn scroll_list_fixture() -> El {
16 let rows: Vec<El> = (0..20)
17 .map(|i| {
18 row([
19 badge(format!("#{i}")).info(),
20 text(format!("Notification {i}")).bold(),
21 spacer(),
22 text(format!("{}m ago", i + 1)).muted(),
23 ])
24 .gap(tokens::SPACE_2)
25 .height(Size::Fixed(44.0))
26 .padding(Sides::xy(tokens::SPACE_3, tokens::SPACE_2))
27 })
28 .collect();
29
30 let list = scroll(rows)
31 .key("notifications")
32 .height(Size::Fixed(420.0))
33 .padding(tokens::SPACE_2);
34
35 column([
36 h2("Notifications"),
37 text("Roll the wheel inside the panel to scroll. The content is taller than the viewport.")
38 .muted(),
39 list,
40 ])
41 .gap(tokens::SPACE_4)
42 .padding(tokens::SPACE_7)
43}48fn fixture() -> El {
49 let centre = h2("Compass").center_text();
50 let dirs = [
51 ("North", "n"),
52 ("NE", "ne"),
53 ("East", "e"),
54 ("SE", "se"),
55 ("South", "s"),
56 ("SW", "sw"),
57 ("West", "w"),
58 ("NW", "nw"),
59 ];
60
61 let mut children: Vec<El> = vec![centre];
62 for (label, k) in dirs {
63 children.push(button(label).key(k).primary());
64 }
65
66 column([
67 h1("Custom layout — circular"),
68 paragraph(
69 "Eight buttons positioned on a circle by an author-supplied \
70 LayoutFn. Stock paint, automatic hover/press, and hit-test \
71 all keep working — only the rect distribution changed.",
72 )
73 .muted(),
74 stack(children)
75 .key("compass")
76 .layout(circular)
77 .width(Size::Fill(1.0))
78 .height(Size::Fixed(360.0)),
79 ])
80 .gap(tokens::SPACE_4)
81 .padding(tokens::SPACE_7)
82}Sourcepub fn pt(self, v: f32) -> Self
pub fn pt(self, v: f32) -> Self
Override only the top padding side, preserving the other three
sides at their current value (whether from a constructor’s
default_padding or a previous explicit .padding(...)).
Mirrors Tailwind’s pt-N. Marks the padding as explicit, so
the metrics pass will not stamp a density-driven value over it.
Sourcepub fn pb(self, v: f32) -> Self
pub fn pb(self, v: f32) -> Self
Override only the bottom padding side. Mirrors Tailwind’s pb-N.
See Self::pt for composition semantics.
Sourcepub fn pl(self, v: f32) -> Self
pub fn pl(self, v: f32) -> Self
Override only the left padding side. Mirrors Tailwind’s pl-N.
See Self::pt for composition semantics.
Sourcepub fn pr(self, v: f32) -> Self
pub fn pr(self, v: f32) -> Self
Override only the right padding side. Mirrors Tailwind’s pr-N.
See Self::pt for composition semantics.
Sourcepub fn px(self, v: f32) -> Self
pub fn px(self, v: f32) -> Self
Override the horizontal padding sides (left + right), preserving
top and bottom. Mirrors Tailwind’s px-N.
See Self::pt for composition semantics.
Sourcepub fn py(self, v: f32) -> Self
pub fn py(self, v: f32) -> Self
Override the vertical padding sides (top + bottom), preserving
left and right. Mirrors Tailwind’s py-N.
See Self::pt for composition semantics.
Sourcepub fn gap(self, g: f32) -> Self
pub fn gap(self, g: f32) -> Self
Examples found in repository?
35fn polish_calibration() -> El {
36 row([sidebar(), main_panel()])
37 .key("metric:root")
38 .gap(0.0)
39 .fill_size()
40 .align(Align::Stretch)
41 .fill(tokens::BACKGROUND)
42}
43
44fn sidebar() -> El {
45 column([
46 column([h2("Aetna"), text("calibration").muted()])
47 .key("metric:sidebar.brand")
48 .gap(tokens::SPACE_1)
49 .height(Size::Hug),
50 spacer().height(Size::Fixed(tokens::SPACE_4)),
51 nav_item("01", "Overview", true),
52 nav_item("02", "Commands", false),
53 nav_item("03", "Tables", false),
54 nav_item("04", "Forms", false),
55 spacer(),
56 badge("dark theme").muted(),
57 ])
58 .gap(tokens::SPACE_2)
59 .padding(tokens::SPACE_5)
60 .key("metric:sidebar")
61 .width(Size::Fixed(220.0))
62 .height(Size::Fill(1.0))
63 .fill(tokens::CARD)
64 .stroke(tokens::BORDER)
65}
66
67fn nav_item(icon: &'static str, label: &'static str, selected: bool) -> El {
68 let mut item = row([
69 icon_cell(icon),
70 text(label)
71 .font_weight(FontWeight::Medium)
72 .ellipsis()
73 .width(Size::Fill(1.0)),
74 ])
75 .key(if selected {
76 "metric:sidebar.nav.row".to_string()
77 } else {
78 format!("nav-{label}")
79 })
80 .metrics_role(MetricsRole::ListItem)
81 .gap(tokens::SPACE_3)
82 .padding(Sides::xy(tokens::SPACE_2, 0.0))
83 .height(Size::Fixed(40.0))
84 .align(Align::Center)
85 .focusable();
86
87 if selected {
88 item = item.current();
89 }
90
91 item
92}
93
94fn main_panel() -> El {
95 column([
96 toolbar(),
97 column([
98 row([
99 kpi_card("Latency", "42 ms", "-18%", true),
100 kpi_card("Runs", "1,284", "+12%", true),
101 kpi_card("Errors", "7", "+2", false),
102 ])
103 .gap(tokens::SPACE_4),
104 row([table_card(), command_card()])
105 .gap(tokens::SPACE_4)
106 .height(Size::Fill(1.0))
107 .align(Align::Stretch),
108 ])
109 .gap(tokens::SPACE_4)
110 .height(Size::Fill(1.0))
111 .align(Align::Stretch),
112 ])
113 .padding(tokens::SPACE_7)
114 .gap(tokens::SPACE_2)
115 .width(Size::Fill(1.0))
116 .height(Size::Fill(1.0))
117}
118
119fn toolbar() -> El {
120 row([
121 column([
122 h1("Polish calibration").key("metric:page.title"),
123 text("A representative app surface for default tuning.")
124 .muted()
125 .key("metric:page.subtitle"),
126 ])
127 .gap(tokens::SPACE_2)
128 .height(Size::Hug),
129 spacer(),
130 button_with_icon("search", "Preview")
131 .secondary()
132 .key("metric:action.secondary"),
133 button_with_icon("upload", "Publish")
134 .primary()
135 .key("metric:action.primary"),
136 ])
137 .key("metric:header")
138 .gap(tokens::SPACE_4)
139 .height(Size::Hug)
140 .align(Align::Start)
141}
142
143fn kpi_card(label: &'static str, value: &'static str, delta: &'static str, positive: bool) -> El {
144 let delta_badge = if positive {
145 badge(delta).success()
146 } else {
147 badge(delta).destructive()
148 };
149 let delta_badge = if label == "Latency" {
150 delta_badge.key("metric:kpi.badge")
151 } else {
152 delta_badge
153 };
154 let value_text = h2(value).display();
155 let value_text = if label == "Latency" {
156 value_text.key("metric:kpi.value")
157 } else {
158 value_text
159 };
160 card([
161 card_header([card_title(label)]),
162 card_content([
163 row([value_text, spacer(), delta_badge]).align(Align::Center),
164 text(if positive {
165 "Moving in the expected direction"
166 } else {
167 "Needs visual attention"
168 })
169 .muted(),
170 ])
171 .gap(tokens::SPACE_6),
172 ])
173 .key(if label == "Latency" {
174 "metric:kpi.card"
175 } else {
176 label
177 })
178 .width(Size::Fill(1.0))
179}
180
181fn table_card() -> El {
182 card([
183 card_header([card_title("Reference rows")]),
184 card_content([table([
185 table_header([table_row([
186 table_head("Status").width(Size::Fixed(86.0)),
187 table_head("Surface").width(Size::Fill(1.0)),
188 table_head("Owner").width(Size::Fixed(110.0)),
189 table_head("State").width(Size::Fixed(86.0)),
190 ])
191 .key("metric:table.header")]),
192 divider(),
193 table_body([
194 data_row("OK", "Settings card", "core", "selected", true, "success"),
195 data_row(
196 "WARN",
197 "Command palette density",
198 "widgets",
199 "needs work",
200 false,
201 "warning",
202 ),
203 data_row(
204 "ERR",
205 "Disabled and invalid states",
206 "style",
207 "missing",
208 false,
209 "destructive",
210 ),
211 data_row(
212 "INFO",
213 "Token resolution",
214 "theme",
215 "planned",
216 false,
217 "info",
218 ),
219 data_row(
220 "OK",
221 "Popover elevation",
222 "shader",
223 "queued",
224 false,
225 "success",
226 ),
227 ])
228 .gap(tokens::SPACE_1)
229 .width(Size::Fill(1.0)),
230 ])]),
231 ])
232 .key("metric:table.card")
233 .width(Size::Fill(1.2))
234 .height(Size::Fill(1.0))
235}
236
237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}More examples
32fn settings_calibration() -> El {
33 row([settings_sidebar(), settings_main()])
34 .key("metric:root")
35 .gap(0.0)
36 .fill_size()
37 .align(Align::Stretch)
38 .fill(tokens::BACKGROUND)
39}
40
41fn settings_sidebar() -> El {
42 column([
43 row([
44 icon_slot("settings"),
45 column([
46 text("Workspace")
47 .semibold()
48 .ellipsis()
49 .width(Size::Fill(1.0)),
50 text("Settings").caption().ellipsis().width(Size::Fill(1.0)),
51 ])
52 .gap(2.0)
53 .width(Size::Fill(1.0))
54 .height(Size::Hug),
55 ])
56 .gap(tokens::SPACE_2)
57 .height(Size::Fixed(44.0))
58 .align(Align::Center),
59 section_label("Personal"),
60 side_item("users", "Profile", false),
61 side_item("settings", "Account", true),
62 side_item("alert-circle", "Security", false),
63 side_item("bell", "Notifications", false),
64 spacer().height(Size::Fixed(tokens::SPACE_4)),
65 section_label("Workspace"),
66 side_item("file-text", "Billing", false),
67 side_item("bar-chart", "Appearance", false),
68 side_item("activity", "Integrations", false),
69 spacer(),
70 column([text("Changes sync after save.").caption().wrap_text()])
71 .padding(tokens::SPACE_2)
72 .fill(tokens::MUTED)
73 .radius(tokens::RADIUS_MD),
74 ])
75 .gap(tokens::SPACE_2)
76 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
77 .key("metric:sidebar")
78 .width(Size::Fixed(244.0))
79 .height(Size::Fill(1.0))
80 .fill(tokens::CARD)
81 .stroke(tokens::BORDER)
82}
83
84fn settings_main() -> El {
85 column([
86 settings_header(),
87 row([settings_nav_card(), settings_body(), settings_aside()])
88 .gap(tokens::SPACE_4)
89 .padding(tokens::SPACE_4)
90 .height(Size::Fill(1.0))
91 .align(Align::Stretch),
92 ])
93 .width(Size::Fill(1.0))
94 .height(Size::Fill(1.0))
95}
96
97fn settings_header() -> El {
98 row([
99 icon_button("menu").ghost(),
100 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
101 h3("Settings").key("metric:page.title"),
102 spacer(),
103 button("Reset").secondary(),
104 button("Save changes").primary(),
105 ])
106 .key("metric:header")
107 .gap(tokens::SPACE_3)
108 .height(Size::Fixed(56.0))
109 .padding(Sides::xy(tokens::SPACE_4, 0.0))
110 .align(Align::Center)
111 .stroke(tokens::BORDER)
112}
113
114fn settings_nav_card() -> El {
115 column([
116 settings_nav_item("Account", true),
117 settings_nav_item("Security", false),
118 settings_nav_item("Notifications", false),
119 settings_nav_item("Appearance", false),
120 settings_nav_item("Billing", false),
121 ])
122 .gap(tokens::SPACE_1)
123 .padding(tokens::SPACE_1)
124 .width(Size::Fixed(220.0))
125 .height(Size::Fill(1.0))
126 .style_profile(StyleProfile::Surface)
127 .surface_role(SurfaceRole::Panel)
128 .fill(tokens::CARD)
129 .stroke(tokens::BORDER)
130 .radius(tokens::RADIUS_MD)
131 .shadow(tokens::SHADOW_MD)
132}
133
134fn settings_nav_item(label: &'static str, selected: bool) -> El {
135 let mut item = row([
136 El::new(Kind::Custom("nav-dot"))
137 .fill(tokens::MUTED_FOREGROUND)
138 .radius(tokens::RADIUS_PILL)
139 .width(Size::Fixed(6.0))
140 .height(Size::Fixed(6.0)),
141 text(label)
142 .font_weight(FontWeight::Medium)
143 .ellipsis()
144 .width(Size::Fill(1.0)),
145 ])
146 .key(if selected {
147 "metric:settings.nav.row".to_string()
148 } else {
149 format!("settings-nav-{label}")
150 })
151 .metrics_role(MetricsRole::ListItem)
152 .align(Align::Center)
153 .focusable();
154
155 if selected {
156 item = item.current();
157 } else {
158 item = item.color(tokens::MUTED_FOREGROUND);
159 }
160
161 item
162}
163
164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}
185
186fn profile_card() -> El {
187 card([
188 card_header([
189 card_title("Profile"),
190 card_description("This information appears in audit logs and shared documents."),
191 ]),
192 card_content([form([
193 row([
194 setting_field("Display name", "Alicia Koch", "display-name"),
195 setting_field("Email", "alicia@acme.co", "email"),
196 ])
197 .gap(tokens::SPACE_3),
198 row([
199 setting_select("Role", "Workspace admin", "role"),
200 setting_select("Region", "US East", "region"),
201 ])
202 .gap(tokens::SPACE_3),
203 ])]),
204 ])
205 .key("metric:profile.card")
206}
207
208fn setting_field(label: &'static str, value: &'static str, key: &'static str) -> El {
209 form_item([
210 form_label(label),
211 form_control(
212 text_input(value, &Selection::caret(key, value.len()), key).key(
213 if key == "display-name" {
214 "metric:form.input"
215 } else {
216 key
217 },
218 ),
219 ),
220 ])
221 .width(Size::Fill(1.0))
222}
223
224fn setting_select(label: &'static str, value: &'static str, key: &'static str) -> El {
225 form_item([form_label(label), form_control(select_trigger(key, value))]).width(Size::Fill(1.0))
226}
227
228fn preferences_card() -> El {
229 card([
230 card_header([
231 card_title("Preferences"),
232 card_description("Defaults used when creating new dashboards and exports."),
233 ]),
234 card_content([column([
235 preference_row(
236 "Compact navigation",
237 "Use tighter rows in the sidebar and command menus.",
238 switch(true).key("compact-navigation"),
239 ),
240 divider(),
241 preference_row(
242 "Email summaries",
243 "Send a daily digest when documents change.",
244 switch(false).key("email-summaries"),
245 ),
246 divider(),
247 preference_row(
248 "Require approval",
249 "Route external sharing through an owner review.",
250 checkbox(true).key("approval-required"),
251 ),
252 ])
253 .gap(0.0)
254 .width(Size::Fill(1.0))])
255 .padding(0.0),
256 ])
257 .key("metric:preferences.card")
258}
259
260fn preference_row(title: &'static str, description: &'static str, control: El) -> El {
261 row([
262 column([
263 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
264 text(description)
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0))
271 .height(Size::Hug),
272 control,
273 ])
274 .key(if title == "Compact navigation" {
275 "metric:preference.row".to_string()
276 } else {
277 format!("preference-{title}")
278 })
279 .metrics_role(MetricsRole::PreferenceRow)
280 .gap(tokens::SPACE_4)
281 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_3))
282 .align(Align::Center)
283}
284
285fn settings_aside() -> El {
286 column([security_card(), scale_card()])
287 .gap(tokens::SPACE_4)
288 .width(Size::Fixed(300.0))
289 .height(Size::Fill(1.0))
290}
291
292fn security_card() -> El {
293 card([
294 card_header([
295 card_title("Security"),
296 card_description("Two-factor authentication is enabled for all privileged users."),
297 ]),
298 card_content([
299 compact_stat("Passkeys", "2 registered", badge("On").success()),
300 compact_stat("Sessions", "3 active", button("Review").secondary()),
301 ]),
302 ])
303 .width(Size::Fill(1.0))
304}
305
306fn scale_card() -> El {
307 card([
308 card_header([
309 card_title("Interface scale"),
310 card_description("Reference captures keep browser zoom fixed and vary root UI scale."),
311 ]),
312 card_content([
313 row([text("Dense").caption(), spacer(), text("Default").caption()]),
314 slider(0.66, tokens::PRIMARY)
315 .key("interface-scale")
316 .width(Size::Fill(1.0)),
317 ]),
318 ])
319 .width(Size::Fill(1.0))
320}
321
322fn compact_stat(title: &'static str, detail: &'static str, control: El) -> El {
323 row([
324 column([
325 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
326 text(detail).caption().ellipsis().width(Size::Fill(1.0)),
327 ])
328 .gap(2.0)
329 .width(Size::Fill(1.0))
330 .height(Size::Hug),
331 control,
332 ])
333 .gap(tokens::SPACE_2)
334 .height(Size::Fixed(44.0))
335 .align(Align::Center)
336}
337
338fn section_label(label: &'static str) -> El {
339 text(label)
340 .caption()
341 .height(Size::Fixed(22.0))
342 .padding(Sides::xy(tokens::SPACE_2, 0.0))
343}
344
345fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
346 let mut item = row([
347 icon(icon_name)
348 .color(tokens::MUTED_FOREGROUND)
349 .icon_size(tokens::ICON_SM)
350 .width(Size::Fixed(tokens::ICON_SM)),
351 text(label)
352 .font_weight(FontWeight::Medium)
353 .ellipsis()
354 .width(Size::Fill(1.0)),
355 ])
356 .key(if selected {
357 "metric:sidebar.nav.row".to_string()
358 } else {
359 format!("side-item-{label}")
360 })
361 .metrics_role(MetricsRole::ListItem)
362 .gap(tokens::SPACE_2)
363 .padding(Sides::xy(tokens::SPACE_2, 0.0))
364 .height(Size::Fixed(32.0))
365 .align(Align::Center)
366 .focusable();
367
368 if selected {
369 item = item.current();
370 } else {
371 item = item.color(tokens::MUTED_FOREGROUND);
372 }
373
374 item
375}32fn dashboard_01_calibration() -> El {
33 row([dashboard_sidebar(), dashboard_main()])
34 .key("metric:root")
35 .gap(0.0)
36 .fill_size()
37 .align(Align::Stretch)
38 .fill(tokens::BACKGROUND)
39}
40
41fn dashboard_sidebar() -> El {
42 column([
43 row([
44 icon_cell("A"),
45 column([
46 text("Acme Inc.")
47 .semibold()
48 .ellipsis()
49 .width(Size::Fill(1.0)),
50 text("Enterprise")
51 .caption()
52 .ellipsis()
53 .width(Size::Fill(1.0)),
54 ])
55 .gap(2.0)
56 .width(Size::Fill(1.0))
57 .height(Size::Hug),
58 ])
59 .gap(tokens::SPACE_2)
60 .height(Size::Fixed(44.0))
61 .align(Align::Center),
62 section_label("Platform"),
63 side_item("layout-dashboard", "Dashboard", true),
64 side_item("activity", "Lifecycle", false),
65 side_item("bar-chart", "Analytics", false),
66 side_item("folder", "Projects", false),
67 spacer().height(Size::Fixed(tokens::SPACE_4)),
68 section_label("Documents"),
69 side_item("file-text", "Data library", false),
70 side_item("download", "Reports", false),
71 side_item("users", "Team", false),
72 spacer(),
73 row([
74 icon_cell("AK"),
75 column([
76 text("Alicia Koch")
77 .semibold()
78 .ellipsis()
79 .width(Size::Fill(1.0)),
80 text("alicia@example.com")
81 .caption()
82 .ellipsis()
83 .width(Size::Fill(1.0)),
84 ])
85 .gap(2.0)
86 .width(Size::Fill(1.0))
87 .height(Size::Hug),
88 ])
89 .gap(tokens::SPACE_2)
90 .height(Size::Fixed(50.0))
91 .align(Align::Center),
92 ])
93 .gap(tokens::SPACE_2)
94 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
95 .key("metric:sidebar")
96 .width(Size::Fixed(244.0))
97 .height(Size::Fill(1.0))
98 .fill(tokens::CARD)
99 .stroke(tokens::BORDER)
100}
101
102fn section_label(label: &'static str) -> El {
103 text(label)
104 .caption()
105 .height(Size::Fixed(22.0))
106 .padding(Sides::xy(tokens::SPACE_2, 0.0))
107}
108
109fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
110 let mut item = row([
111 icon(icon_name)
112 .color(tokens::MUTED_FOREGROUND)
113 .icon_size(tokens::ICON_SM)
114 .width(Size::Fixed(tokens::ICON_SM)),
115 text(label)
116 .font_weight(FontWeight::Medium)
117 .ellipsis()
118 .width(Size::Fill(1.0)),
119 ])
120 .key(if selected {
121 "metric:sidebar.nav.row".to_string()
122 } else {
123 format!("side-item-{label}")
124 })
125 .metrics_role(MetricsRole::ListItem)
126 .gap(tokens::SPACE_2)
127 .padding(Sides::xy(tokens::SPACE_2, 0.0))
128 .height(Size::Fixed(32.0))
129 .align(Align::Center)
130 .focusable();
131
132 if selected {
133 item = item.current();
134 } else {
135 item = item.color(tokens::MUTED_FOREGROUND);
136 }
137
138 item
139}
140
141fn dashboard_main() -> El {
142 column([
143 dashboard_header(),
144 column([
145 row([
146 metric_card(
147 "bar-chart",
148 "Total Revenue",
149 "$1,250.00",
150 "+12.5%",
151 "Trending up this month",
152 true,
153 ),
154 metric_card(
155 "users",
156 "New Customers",
157 "1,234",
158 "-20%",
159 "Acquisition needs attention",
160 false,
161 ),
162 metric_card(
163 "folder",
164 "Active Accounts",
165 "45,678",
166 "+12.5%",
167 "Strong user retention",
168 true,
169 ),
170 metric_card(
171 "activity",
172 "Growth Rate",
173 "4.5%",
174 "+4.5%",
175 "Meets growth projections",
176 true,
177 ),
178 ])
179 .gap(tokens::SPACE_4),
180 row([chart_card(), sales_card()])
181 .gap(tokens::SPACE_4)
182 .height(Size::Fixed(306.0))
183 .align(Align::Stretch),
184 documents_card(),
185 ])
186 .gap(tokens::SPACE_4)
187 .padding(tokens::SPACE_7)
188 .height(Size::Fill(1.0)),
189 ])
190 .width(Size::Fill(1.0))
191 .height(Size::Fill(1.0))
192}
193
194fn dashboard_header() -> El {
195 row([
196 icon_button("menu").ghost(),
197 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
198 h3("Documents").key("metric:page.title"),
199 spacer(),
200 text_input("Search...", &Selection::default(), "dashboard-search")
201 .key("metric:command.input")
202 .width(Size::Fixed(260.0)),
203 icon_button("plus").ghost(),
204 icon_button("bell").ghost(),
205 ])
206 .key("metric:header")
207 .gap(tokens::SPACE_3)
208 .height(Size::Fixed(56.0))
209 .padding(Sides::xy(tokens::SPACE_4, 0.0))
210 .align(Align::Center)
211 .stroke(tokens::BORDER)
212}
213
214fn metric_card(
215 icon_name: &'static str,
216 title: &'static str,
217 value: &'static str,
218 delta: &'static str,
219 note: &'static str,
220 positive: bool,
221) -> El {
222 let badge = if positive {
223 badge(delta).success()
224 } else {
225 badge(delta).warning()
226 };
227 let badge = if title == "Total Revenue" {
228 badge.key("metric:kpi.badge")
229 } else {
230 badge
231 };
232 let value = if title == "Total Revenue" {
233 h2(value).ellipsis().key("metric:kpi.value")
234 } else {
235 h2(value).ellipsis()
236 };
237 card([card_content([
238 row([
239 row([
240 icon(icon_name)
241 .color(tokens::MUTED_FOREGROUND)
242 .icon_size(tokens::ICON_XS),
243 text(title).muted().ellipsis().width(Size::Fill(1.0)),
244 ])
245 .gap(tokens::SPACE_1)
246 .width(Size::Fill(1.0))
247 .align(Align::Center),
248 badge,
249 ])
250 .gap(tokens::SPACE_2)
251 .align(Align::Center),
252 value,
253 text(note).caption().ellipsis().width(Size::Fill(1.0)),
254 ])
255 .padding(tokens::SPACE_4)
256 .gap(tokens::SPACE_2)])
257 .key(if title == "Total Revenue" {
258 "metric:kpi.card"
259 } else {
260 title
261 })
262 .width(Size::Fill(1.0))
263}
264
265fn chart_card() -> El {
266 card([
267 card_header([
268 card_title("Visitors for the last 6 months"),
269 card_description("Total visitors by channel."),
270 ])
271 .padding(tokens::SPACE_4),
272 card_content([row(chart_bars())
273 .gap(2.0)
274 .height(Size::Fill(1.0))
275 .align(Align::End)])
276 .padding(Sides {
277 left: tokens::SPACE_4,
278 right: tokens::SPACE_4,
279 top: 0.0,
280 bottom: tokens::SPACE_4,
281 })
282 .height(Size::Fill(1.0)),
283 ])
284 .key("metric:chart.card")
285 .width(Size::Fill(1.0))
286 .height(Size::Fill(1.0))
287}
288
289fn chart_bars() -> Vec<El> {
290 [
291 48.0, 72.0, 56.0, 90.0, 64.0, 80.0, 108.0, 84.0, 122.0, 96.0, 136.0, 118.0,
292 ]
293 .into_iter()
294 .flat_map(|height| {
295 [
296 bar(height, tokens::MUTED_FOREGROUND),
297 bar((height - 28.0_f32).max(24.0), tokens::INPUT),
298 ]
299 })
300 .collect()
301}
302
303fn bar(height: f32, color: Color) -> El {
304 El::new(Kind::Custom("chart_bar"))
305 .fill(color)
306 .radius(tokens::RADIUS_SM)
307 .width(Size::Fill(1.0))
308 .height(Size::Fixed(height))
309}
310
311fn sales_card() -> El {
312 card([
313 card_header([
314 card_title("Recent Sales"),
315 card_description("You made 265 sales this month."),
316 ])
317 .padding(tokens::SPACE_4),
318 card_content([
319 sale_row("OM", "Olivia Martin", "olivia@example.com", "+$1,999.00"),
320 sale_row("JL", "Jackson Lee", "jackson@example.com", "+$39.00"),
321 sale_row("IN", "Isabella Nguyen", "isabella@example.com", "+$299.00"),
322 sale_row("WK", "William Kim", "will@example.com", "+$99.00"),
323 ])
324 .gap(tokens::SPACE_2)
325 .padding(Sides {
326 left: tokens::SPACE_4,
327 right: tokens::SPACE_4,
328 top: 0.0,
329 bottom: tokens::SPACE_4,
330 }),
331 ])
332 .key("metric:sales.card")
333 .width(Size::Fixed(330.0))
334 .height(Size::Fill(1.0))
335}
336
337fn sale_row(
338 initials: &'static str,
339 name: &'static str,
340 email: &'static str,
341 amount: &'static str,
342) -> El {
343 row([
344 icon_cell(initials),
345 column([
346 text(name).semibold().ellipsis().width(Size::Fill(1.0)),
347 text(email).caption().ellipsis().width(Size::Fill(1.0)),
348 ])
349 .gap(2.0)
350 .height(Size::Hug)
351 .width(Size::Fill(1.0)),
352 text(amount).label().small(),
353 ])
354 .gap(tokens::SPACE_2)
355 .height(Size::Fixed(42.0))
356 .align(Align::Center)
357}
358
359fn documents_card() -> El {
360 card([
361 card_header([card_title("Documents")]).padding(tokens::SPACE_4),
362 card_content([scroll([table([
363 table_header([table_row([
364 table_head("").width(Size::Fixed(35.0)),
365 table_head("Header").width(Size::Fill(1.8)),
366 table_head("Section Type").width(Size::Fill(1.0)),
367 table_head("Status").width(Size::Fixed(104.0)),
368 table_head("Target").width(Size::Fixed(64.0)),
369 table_head("Limit").width(Size::Fixed(64.0)),
370 table_head("Reviewer").width(Size::Fixed(128.0)),
371 table_head("").width(Size::Fixed(32.0)),
372 ])
373 .padding(Sides::xy(tokens::SPACE_4, 0.0))
374 .key("metric:table.header")]),
375 divider(),
376 table_body([
377 document_row(
378 "Cover page",
379 "Cover page",
380 "In Process",
381 "18",
382 "5",
383 "Eddie Lake",
384 "info",
385 ),
386 document_row(
387 "Table of contents",
388 "Table of contents",
389 "Done",
390 "29",
391 "24",
392 "Eddie Lake",
393 "success",
394 ),
395 ]),
396 ])])
397 .height(Size::Fill(1.0))])
398 .gap(0.0)
399 .padding(0.0)
400 .height(Size::Fill(1.0)),
401 ])
402 .key("metric:table.card")
403 .height(Size::Fill(1.0))
404}20fn list_rows() -> Vec<El> {
21 (0..40)
22 .map(|i| {
23 row([
24 text(format!("{i:02}.")).mono().muted(),
25 text(format!("scrollable list item {i}")),
26 ])
27 .gap(tokens::SPACE_2)
28 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_1))
29 .height(Size::Fixed(28.0))
30 .align(Align::Center)
31 })
32 .collect()
33}
34
35fn fixture() -> El {
36 column([
37 h2("Scrollbar"),
38 text("scroll() and virtual_list() show a draggable thumb by default.").muted(),
39 row([
40 // 1) scroll() — default-on scrollbar.
41 column([
42 text("scroll() — default").bold(),
43 scroll(list_rows())
44 .height(Size::Fixed(240.0))
45 .padding(tokens::SPACE_2)
46 .stroke(tokens::BORDER)
47 .stroke_width(1.0)
48 .radius(tokens::RADIUS_MD),
49 ])
50 .gap(tokens::SPACE_2)
51 .width(Size::Fill(1.0))
52 .height(Size::Hug),
53 // 2) virtual_list — thumb scales to content size.
54 column([
55 text("virtual_list(200, 28)").bold(),
56 virtual_list(200, 28.0, |i| {
57 row([
58 text(format!("{i:03}")).mono().muted(),
59 text(format!("row {i}")),
60 ])
61 .gap(tokens::SPACE_2)
62 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_1))
63 .height(Size::Fixed(28.0))
64 .align(Align::Center)
65 })
66 .height(Size::Fixed(240.0))
67 .padding(tokens::SPACE_2)
68 .stroke(tokens::BORDER)
69 .stroke_width(1.0)
70 .radius(tokens::RADIUS_MD),
71 ])
72 .gap(tokens::SPACE_2)
73 .width(Size::Fill(1.0))
74 .height(Size::Hug),
75 // 3) Opt-out: same content, no thumb.
76 column([
77 text("scroll().no_scrollbar()").bold(),
78 scroll(list_rows())
79 .no_scrollbar()
80 .height(Size::Fixed(240.0))
81 .padding(tokens::SPACE_2)
82 .stroke(tokens::BORDER)
83 .stroke_width(1.0)
84 .radius(tokens::RADIUS_MD),
85 ])
86 .gap(tokens::SPACE_2)
87 .width(Size::Fill(1.0))
88 .height(Size::Hug),
89 ])
90 .gap(tokens::SPACE_4)
91 .width(Size::Fill(1.0)),
92 ])
93 .gap(tokens::SPACE_4)
94 .padding(tokens::SPACE_7)
95}68fn build_row(c: &FakeCommit, idx: usize, selected: bool) -> El {
69 row([
70 graph_cell(c.lane, selected),
71 text(c.sha).mono().muted(),
72 text(c.subject),
73 spacer(),
74 text(format!("{} · {}", c.author, c.when)).muted(),
75 ])
76 .key(format!("commit-{idx}"))
77 .gap(tokens::SPACE_3)
78 .padding(Sides::xy(tokens::SPACE_2, 0.0))
79 .height(Size::Fixed(ROW_HEIGHT))
80 .align(Align::Center)
81}
82
83fn fixture() -> El {
84 #[rustfmt::skip]
85 let commits = [
86 FakeCommit { sha: "8a3f1c9", subject: "fix race condition in scheduler", author: "ada", when: "12m", lane: 0 },
87 FakeCommit { sha: "1b07d4e", subject: "tweak token tooltip wording", author: "linus", when: "1h", lane: 0 },
88 FakeCommit { sha: "9f2e4a1", subject: "wire avatar fallback identicon", author: "joelle", when: "3h", lane: 1 },
89 FakeCommit { sha: "44ab8d2", subject: "diff: word-level highlight pass", author: "raphael", when: "5h", lane: 1 },
90 FakeCommit { sha: "61c0fe7", subject: "ci: bump rust toolchain to 1.85", author: "mei", when: "7h", lane: 2 },
91 FakeCommit { sha: "a90215b", subject: "switch logging to env_logger", author: "isabel", when: "1d", lane: 2 },
92 FakeCommit { sha: "0d7e3c4", subject: "drop unused commit_detail cache", author: "noor", when: "1d", lane: 1 },
93 FakeCommit { sha: "33b2118", subject: "context-menu spacing pass", author: "kira", when: "2d", lane: 3 },
94 ];
95 let selected_idx = 3;
96 let rows = commits
97 .iter()
98 .enumerate()
99 .map(|(i, c)| build_row(c, i, i == selected_idx))
100 .collect::<Vec<_>>();
101
102 column([
103 h2("Custom-painted commit graph"),
104 text("8 commits · custom shader paints lane line + circle node").muted(),
105 column(rows).gap(0.0),
106 ])
107 .padding(tokens::SPACE_4)
108 .gap(tokens::SPACE_2)
109}18fn build_row(i: usize) -> El {
19 let badge_el = match i % 5 {
20 0 => badge("info").muted(),
21 1 => badge("warn").warning(),
22 2 => badge("ok").success(),
23 3 => badge("err").destructive(),
24 _ => spacer(),
25 };
26 row([
27 text(format!("#{i:05}")).mono(),
28 spacer(),
29 text(format!("entry {i}")),
30 spacer(),
31 badge_el,
32 ])
33 .key(format!("row-{i}"))
34 .gap(tokens::SPACE_3)
35 .padding(Sides::xy(tokens::SPACE_3, tokens::SPACE_2))
36 .height(Size::Fixed(ROW_HEIGHT))
37}
38
39fn fixture() -> El {
40 column([
41 h1("Virtualized list"),
42 paragraph(format!(
43 "{ROW_COUNT} rows × {ROW_HEIGHT}px in a windowed viewport. \
44 Only the rows intersecting the viewport are realized — see \
45 `tree.txt` for proof."
46 ))
47 .muted(),
48 virtual_list(ROW_COUNT, ROW_HEIGHT, build_row)
49 .key("entries")
50 .height(Size::Fill(1.0)),
51 ])
52 .gap(tokens::SPACE_4)
53 .padding(tokens::SPACE_7)
54}Sourcepub fn align(self, a: Align) -> Self
pub fn align(self, a: Align) -> Self
Examples found in repository?
35fn polish_calibration() -> El {
36 row([sidebar(), main_panel()])
37 .key("metric:root")
38 .gap(0.0)
39 .fill_size()
40 .align(Align::Stretch)
41 .fill(tokens::BACKGROUND)
42}
43
44fn sidebar() -> El {
45 column([
46 column([h2("Aetna"), text("calibration").muted()])
47 .key("metric:sidebar.brand")
48 .gap(tokens::SPACE_1)
49 .height(Size::Hug),
50 spacer().height(Size::Fixed(tokens::SPACE_4)),
51 nav_item("01", "Overview", true),
52 nav_item("02", "Commands", false),
53 nav_item("03", "Tables", false),
54 nav_item("04", "Forms", false),
55 spacer(),
56 badge("dark theme").muted(),
57 ])
58 .gap(tokens::SPACE_2)
59 .padding(tokens::SPACE_5)
60 .key("metric:sidebar")
61 .width(Size::Fixed(220.0))
62 .height(Size::Fill(1.0))
63 .fill(tokens::CARD)
64 .stroke(tokens::BORDER)
65}
66
67fn nav_item(icon: &'static str, label: &'static str, selected: bool) -> El {
68 let mut item = row([
69 icon_cell(icon),
70 text(label)
71 .font_weight(FontWeight::Medium)
72 .ellipsis()
73 .width(Size::Fill(1.0)),
74 ])
75 .key(if selected {
76 "metric:sidebar.nav.row".to_string()
77 } else {
78 format!("nav-{label}")
79 })
80 .metrics_role(MetricsRole::ListItem)
81 .gap(tokens::SPACE_3)
82 .padding(Sides::xy(tokens::SPACE_2, 0.0))
83 .height(Size::Fixed(40.0))
84 .align(Align::Center)
85 .focusable();
86
87 if selected {
88 item = item.current();
89 }
90
91 item
92}
93
94fn main_panel() -> El {
95 column([
96 toolbar(),
97 column([
98 row([
99 kpi_card("Latency", "42 ms", "-18%", true),
100 kpi_card("Runs", "1,284", "+12%", true),
101 kpi_card("Errors", "7", "+2", false),
102 ])
103 .gap(tokens::SPACE_4),
104 row([table_card(), command_card()])
105 .gap(tokens::SPACE_4)
106 .height(Size::Fill(1.0))
107 .align(Align::Stretch),
108 ])
109 .gap(tokens::SPACE_4)
110 .height(Size::Fill(1.0))
111 .align(Align::Stretch),
112 ])
113 .padding(tokens::SPACE_7)
114 .gap(tokens::SPACE_2)
115 .width(Size::Fill(1.0))
116 .height(Size::Fill(1.0))
117}
118
119fn toolbar() -> El {
120 row([
121 column([
122 h1("Polish calibration").key("metric:page.title"),
123 text("A representative app surface for default tuning.")
124 .muted()
125 .key("metric:page.subtitle"),
126 ])
127 .gap(tokens::SPACE_2)
128 .height(Size::Hug),
129 spacer(),
130 button_with_icon("search", "Preview")
131 .secondary()
132 .key("metric:action.secondary"),
133 button_with_icon("upload", "Publish")
134 .primary()
135 .key("metric:action.primary"),
136 ])
137 .key("metric:header")
138 .gap(tokens::SPACE_4)
139 .height(Size::Hug)
140 .align(Align::Start)
141}
142
143fn kpi_card(label: &'static str, value: &'static str, delta: &'static str, positive: bool) -> El {
144 let delta_badge = if positive {
145 badge(delta).success()
146 } else {
147 badge(delta).destructive()
148 };
149 let delta_badge = if label == "Latency" {
150 delta_badge.key("metric:kpi.badge")
151 } else {
152 delta_badge
153 };
154 let value_text = h2(value).display();
155 let value_text = if label == "Latency" {
156 value_text.key("metric:kpi.value")
157 } else {
158 value_text
159 };
160 card([
161 card_header([card_title(label)]),
162 card_content([
163 row([value_text, spacer(), delta_badge]).align(Align::Center),
164 text(if positive {
165 "Moving in the expected direction"
166 } else {
167 "Needs visual attention"
168 })
169 .muted(),
170 ])
171 .gap(tokens::SPACE_6),
172 ])
173 .key(if label == "Latency" {
174 "metric:kpi.card"
175 } else {
176 label
177 })
178 .width(Size::Fill(1.0))
179}More examples
32fn settings_calibration() -> El {
33 row([settings_sidebar(), settings_main()])
34 .key("metric:root")
35 .gap(0.0)
36 .fill_size()
37 .align(Align::Stretch)
38 .fill(tokens::BACKGROUND)
39}
40
41fn settings_sidebar() -> El {
42 column([
43 row([
44 icon_slot("settings"),
45 column([
46 text("Workspace")
47 .semibold()
48 .ellipsis()
49 .width(Size::Fill(1.0)),
50 text("Settings").caption().ellipsis().width(Size::Fill(1.0)),
51 ])
52 .gap(2.0)
53 .width(Size::Fill(1.0))
54 .height(Size::Hug),
55 ])
56 .gap(tokens::SPACE_2)
57 .height(Size::Fixed(44.0))
58 .align(Align::Center),
59 section_label("Personal"),
60 side_item("users", "Profile", false),
61 side_item("settings", "Account", true),
62 side_item("alert-circle", "Security", false),
63 side_item("bell", "Notifications", false),
64 spacer().height(Size::Fixed(tokens::SPACE_4)),
65 section_label("Workspace"),
66 side_item("file-text", "Billing", false),
67 side_item("bar-chart", "Appearance", false),
68 side_item("activity", "Integrations", false),
69 spacer(),
70 column([text("Changes sync after save.").caption().wrap_text()])
71 .padding(tokens::SPACE_2)
72 .fill(tokens::MUTED)
73 .radius(tokens::RADIUS_MD),
74 ])
75 .gap(tokens::SPACE_2)
76 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
77 .key("metric:sidebar")
78 .width(Size::Fixed(244.0))
79 .height(Size::Fill(1.0))
80 .fill(tokens::CARD)
81 .stroke(tokens::BORDER)
82}
83
84fn settings_main() -> El {
85 column([
86 settings_header(),
87 row([settings_nav_card(), settings_body(), settings_aside()])
88 .gap(tokens::SPACE_4)
89 .padding(tokens::SPACE_4)
90 .height(Size::Fill(1.0))
91 .align(Align::Stretch),
92 ])
93 .width(Size::Fill(1.0))
94 .height(Size::Fill(1.0))
95}
96
97fn settings_header() -> El {
98 row([
99 icon_button("menu").ghost(),
100 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
101 h3("Settings").key("metric:page.title"),
102 spacer(),
103 button("Reset").secondary(),
104 button("Save changes").primary(),
105 ])
106 .key("metric:header")
107 .gap(tokens::SPACE_3)
108 .height(Size::Fixed(56.0))
109 .padding(Sides::xy(tokens::SPACE_4, 0.0))
110 .align(Align::Center)
111 .stroke(tokens::BORDER)
112}
113
114fn settings_nav_card() -> El {
115 column([
116 settings_nav_item("Account", true),
117 settings_nav_item("Security", false),
118 settings_nav_item("Notifications", false),
119 settings_nav_item("Appearance", false),
120 settings_nav_item("Billing", false),
121 ])
122 .gap(tokens::SPACE_1)
123 .padding(tokens::SPACE_1)
124 .width(Size::Fixed(220.0))
125 .height(Size::Fill(1.0))
126 .style_profile(StyleProfile::Surface)
127 .surface_role(SurfaceRole::Panel)
128 .fill(tokens::CARD)
129 .stroke(tokens::BORDER)
130 .radius(tokens::RADIUS_MD)
131 .shadow(tokens::SHADOW_MD)
132}
133
134fn settings_nav_item(label: &'static str, selected: bool) -> El {
135 let mut item = row([
136 El::new(Kind::Custom("nav-dot"))
137 .fill(tokens::MUTED_FOREGROUND)
138 .radius(tokens::RADIUS_PILL)
139 .width(Size::Fixed(6.0))
140 .height(Size::Fixed(6.0)),
141 text(label)
142 .font_weight(FontWeight::Medium)
143 .ellipsis()
144 .width(Size::Fill(1.0)),
145 ])
146 .key(if selected {
147 "metric:settings.nav.row".to_string()
148 } else {
149 format!("settings-nav-{label}")
150 })
151 .metrics_role(MetricsRole::ListItem)
152 .align(Align::Center)
153 .focusable();
154
155 if selected {
156 item = item.current();
157 } else {
158 item = item.color(tokens::MUTED_FOREGROUND);
159 }
160
161 item
162}
163
164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}
185
186fn profile_card() -> El {
187 card([
188 card_header([
189 card_title("Profile"),
190 card_description("This information appears in audit logs and shared documents."),
191 ]),
192 card_content([form([
193 row([
194 setting_field("Display name", "Alicia Koch", "display-name"),
195 setting_field("Email", "alicia@acme.co", "email"),
196 ])
197 .gap(tokens::SPACE_3),
198 row([
199 setting_select("Role", "Workspace admin", "role"),
200 setting_select("Region", "US East", "region"),
201 ])
202 .gap(tokens::SPACE_3),
203 ])]),
204 ])
205 .key("metric:profile.card")
206}
207
208fn setting_field(label: &'static str, value: &'static str, key: &'static str) -> El {
209 form_item([
210 form_label(label),
211 form_control(
212 text_input(value, &Selection::caret(key, value.len()), key).key(
213 if key == "display-name" {
214 "metric:form.input"
215 } else {
216 key
217 },
218 ),
219 ),
220 ])
221 .width(Size::Fill(1.0))
222}
223
224fn setting_select(label: &'static str, value: &'static str, key: &'static str) -> El {
225 form_item([form_label(label), form_control(select_trigger(key, value))]).width(Size::Fill(1.0))
226}
227
228fn preferences_card() -> El {
229 card([
230 card_header([
231 card_title("Preferences"),
232 card_description("Defaults used when creating new dashboards and exports."),
233 ]),
234 card_content([column([
235 preference_row(
236 "Compact navigation",
237 "Use tighter rows in the sidebar and command menus.",
238 switch(true).key("compact-navigation"),
239 ),
240 divider(),
241 preference_row(
242 "Email summaries",
243 "Send a daily digest when documents change.",
244 switch(false).key("email-summaries"),
245 ),
246 divider(),
247 preference_row(
248 "Require approval",
249 "Route external sharing through an owner review.",
250 checkbox(true).key("approval-required"),
251 ),
252 ])
253 .gap(0.0)
254 .width(Size::Fill(1.0))])
255 .padding(0.0),
256 ])
257 .key("metric:preferences.card")
258}
259
260fn preference_row(title: &'static str, description: &'static str, control: El) -> El {
261 row([
262 column([
263 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
264 text(description)
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0))
271 .height(Size::Hug),
272 control,
273 ])
274 .key(if title == "Compact navigation" {
275 "metric:preference.row".to_string()
276 } else {
277 format!("preference-{title}")
278 })
279 .metrics_role(MetricsRole::PreferenceRow)
280 .gap(tokens::SPACE_4)
281 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_3))
282 .align(Align::Center)
283}
284
285fn settings_aside() -> El {
286 column([security_card(), scale_card()])
287 .gap(tokens::SPACE_4)
288 .width(Size::Fixed(300.0))
289 .height(Size::Fill(1.0))
290}
291
292fn security_card() -> El {
293 card([
294 card_header([
295 card_title("Security"),
296 card_description("Two-factor authentication is enabled for all privileged users."),
297 ]),
298 card_content([
299 compact_stat("Passkeys", "2 registered", badge("On").success()),
300 compact_stat("Sessions", "3 active", button("Review").secondary()),
301 ]),
302 ])
303 .width(Size::Fill(1.0))
304}
305
306fn scale_card() -> El {
307 card([
308 card_header([
309 card_title("Interface scale"),
310 card_description("Reference captures keep browser zoom fixed and vary root UI scale."),
311 ]),
312 card_content([
313 row([text("Dense").caption(), spacer(), text("Default").caption()]),
314 slider(0.66, tokens::PRIMARY)
315 .key("interface-scale")
316 .width(Size::Fill(1.0)),
317 ]),
318 ])
319 .width(Size::Fill(1.0))
320}
321
322fn compact_stat(title: &'static str, detail: &'static str, control: El) -> El {
323 row([
324 column([
325 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
326 text(detail).caption().ellipsis().width(Size::Fill(1.0)),
327 ])
328 .gap(2.0)
329 .width(Size::Fill(1.0))
330 .height(Size::Hug),
331 control,
332 ])
333 .gap(tokens::SPACE_2)
334 .height(Size::Fixed(44.0))
335 .align(Align::Center)
336}
337
338fn section_label(label: &'static str) -> El {
339 text(label)
340 .caption()
341 .height(Size::Fixed(22.0))
342 .padding(Sides::xy(tokens::SPACE_2, 0.0))
343}
344
345fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
346 let mut item = row([
347 icon(icon_name)
348 .color(tokens::MUTED_FOREGROUND)
349 .icon_size(tokens::ICON_SM)
350 .width(Size::Fixed(tokens::ICON_SM)),
351 text(label)
352 .font_weight(FontWeight::Medium)
353 .ellipsis()
354 .width(Size::Fill(1.0)),
355 ])
356 .key(if selected {
357 "metric:sidebar.nav.row".to_string()
358 } else {
359 format!("side-item-{label}")
360 })
361 .metrics_role(MetricsRole::ListItem)
362 .gap(tokens::SPACE_2)
363 .padding(Sides::xy(tokens::SPACE_2, 0.0))
364 .height(Size::Fixed(32.0))
365 .align(Align::Center)
366 .focusable();
367
368 if selected {
369 item = item.current();
370 } else {
371 item = item.color(tokens::MUTED_FOREGROUND);
372 }
373
374 item
375}
376
377fn icon_slot(icon_name: &'static str) -> El {
378 El::new(Kind::Custom("icon_cell"))
379 .style_profile(StyleProfile::Surface)
380 .child(
381 icon(icon_name)
382 .color(tokens::FOREGROUND)
383 .icon_size(tokens::ICON_XS),
384 )
385 .align(Align::Center)
386 .justify(Justify::Center)
387 .fill(tokens::MUTED)
388 .stroke(tokens::BORDER)
389 .radius(tokens::RADIUS_SM)
390 .width(Size::Fixed(30.0))
391 .height(Size::Fixed(30.0))
392}32fn dashboard_01_calibration() -> El {
33 row([dashboard_sidebar(), dashboard_main()])
34 .key("metric:root")
35 .gap(0.0)
36 .fill_size()
37 .align(Align::Stretch)
38 .fill(tokens::BACKGROUND)
39}
40
41fn dashboard_sidebar() -> El {
42 column([
43 row([
44 icon_cell("A"),
45 column([
46 text("Acme Inc.")
47 .semibold()
48 .ellipsis()
49 .width(Size::Fill(1.0)),
50 text("Enterprise")
51 .caption()
52 .ellipsis()
53 .width(Size::Fill(1.0)),
54 ])
55 .gap(2.0)
56 .width(Size::Fill(1.0))
57 .height(Size::Hug),
58 ])
59 .gap(tokens::SPACE_2)
60 .height(Size::Fixed(44.0))
61 .align(Align::Center),
62 section_label("Platform"),
63 side_item("layout-dashboard", "Dashboard", true),
64 side_item("activity", "Lifecycle", false),
65 side_item("bar-chart", "Analytics", false),
66 side_item("folder", "Projects", false),
67 spacer().height(Size::Fixed(tokens::SPACE_4)),
68 section_label("Documents"),
69 side_item("file-text", "Data library", false),
70 side_item("download", "Reports", false),
71 side_item("users", "Team", false),
72 spacer(),
73 row([
74 icon_cell("AK"),
75 column([
76 text("Alicia Koch")
77 .semibold()
78 .ellipsis()
79 .width(Size::Fill(1.0)),
80 text("alicia@example.com")
81 .caption()
82 .ellipsis()
83 .width(Size::Fill(1.0)),
84 ])
85 .gap(2.0)
86 .width(Size::Fill(1.0))
87 .height(Size::Hug),
88 ])
89 .gap(tokens::SPACE_2)
90 .height(Size::Fixed(50.0))
91 .align(Align::Center),
92 ])
93 .gap(tokens::SPACE_2)
94 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
95 .key("metric:sidebar")
96 .width(Size::Fixed(244.0))
97 .height(Size::Fill(1.0))
98 .fill(tokens::CARD)
99 .stroke(tokens::BORDER)
100}
101
102fn section_label(label: &'static str) -> El {
103 text(label)
104 .caption()
105 .height(Size::Fixed(22.0))
106 .padding(Sides::xy(tokens::SPACE_2, 0.0))
107}
108
109fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
110 let mut item = row([
111 icon(icon_name)
112 .color(tokens::MUTED_FOREGROUND)
113 .icon_size(tokens::ICON_SM)
114 .width(Size::Fixed(tokens::ICON_SM)),
115 text(label)
116 .font_weight(FontWeight::Medium)
117 .ellipsis()
118 .width(Size::Fill(1.0)),
119 ])
120 .key(if selected {
121 "metric:sidebar.nav.row".to_string()
122 } else {
123 format!("side-item-{label}")
124 })
125 .metrics_role(MetricsRole::ListItem)
126 .gap(tokens::SPACE_2)
127 .padding(Sides::xy(tokens::SPACE_2, 0.0))
128 .height(Size::Fixed(32.0))
129 .align(Align::Center)
130 .focusable();
131
132 if selected {
133 item = item.current();
134 } else {
135 item = item.color(tokens::MUTED_FOREGROUND);
136 }
137
138 item
139}
140
141fn dashboard_main() -> El {
142 column([
143 dashboard_header(),
144 column([
145 row([
146 metric_card(
147 "bar-chart",
148 "Total Revenue",
149 "$1,250.00",
150 "+12.5%",
151 "Trending up this month",
152 true,
153 ),
154 metric_card(
155 "users",
156 "New Customers",
157 "1,234",
158 "-20%",
159 "Acquisition needs attention",
160 false,
161 ),
162 metric_card(
163 "folder",
164 "Active Accounts",
165 "45,678",
166 "+12.5%",
167 "Strong user retention",
168 true,
169 ),
170 metric_card(
171 "activity",
172 "Growth Rate",
173 "4.5%",
174 "+4.5%",
175 "Meets growth projections",
176 true,
177 ),
178 ])
179 .gap(tokens::SPACE_4),
180 row([chart_card(), sales_card()])
181 .gap(tokens::SPACE_4)
182 .height(Size::Fixed(306.0))
183 .align(Align::Stretch),
184 documents_card(),
185 ])
186 .gap(tokens::SPACE_4)
187 .padding(tokens::SPACE_7)
188 .height(Size::Fill(1.0)),
189 ])
190 .width(Size::Fill(1.0))
191 .height(Size::Fill(1.0))
192}
193
194fn dashboard_header() -> El {
195 row([
196 icon_button("menu").ghost(),
197 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
198 h3("Documents").key("metric:page.title"),
199 spacer(),
200 text_input("Search...", &Selection::default(), "dashboard-search")
201 .key("metric:command.input")
202 .width(Size::Fixed(260.0)),
203 icon_button("plus").ghost(),
204 icon_button("bell").ghost(),
205 ])
206 .key("metric:header")
207 .gap(tokens::SPACE_3)
208 .height(Size::Fixed(56.0))
209 .padding(Sides::xy(tokens::SPACE_4, 0.0))
210 .align(Align::Center)
211 .stroke(tokens::BORDER)
212}
213
214fn metric_card(
215 icon_name: &'static str,
216 title: &'static str,
217 value: &'static str,
218 delta: &'static str,
219 note: &'static str,
220 positive: bool,
221) -> El {
222 let badge = if positive {
223 badge(delta).success()
224 } else {
225 badge(delta).warning()
226 };
227 let badge = if title == "Total Revenue" {
228 badge.key("metric:kpi.badge")
229 } else {
230 badge
231 };
232 let value = if title == "Total Revenue" {
233 h2(value).ellipsis().key("metric:kpi.value")
234 } else {
235 h2(value).ellipsis()
236 };
237 card([card_content([
238 row([
239 row([
240 icon(icon_name)
241 .color(tokens::MUTED_FOREGROUND)
242 .icon_size(tokens::ICON_XS),
243 text(title).muted().ellipsis().width(Size::Fill(1.0)),
244 ])
245 .gap(tokens::SPACE_1)
246 .width(Size::Fill(1.0))
247 .align(Align::Center),
248 badge,
249 ])
250 .gap(tokens::SPACE_2)
251 .align(Align::Center),
252 value,
253 text(note).caption().ellipsis().width(Size::Fill(1.0)),
254 ])
255 .padding(tokens::SPACE_4)
256 .gap(tokens::SPACE_2)])
257 .key(if title == "Total Revenue" {
258 "metric:kpi.card"
259 } else {
260 title
261 })
262 .width(Size::Fill(1.0))
263}
264
265fn chart_card() -> El {
266 card([
267 card_header([
268 card_title("Visitors for the last 6 months"),
269 card_description("Total visitors by channel."),
270 ])
271 .padding(tokens::SPACE_4),
272 card_content([row(chart_bars())
273 .gap(2.0)
274 .height(Size::Fill(1.0))
275 .align(Align::End)])
276 .padding(Sides {
277 left: tokens::SPACE_4,
278 right: tokens::SPACE_4,
279 top: 0.0,
280 bottom: tokens::SPACE_4,
281 })
282 .height(Size::Fill(1.0)),
283 ])
284 .key("metric:chart.card")
285 .width(Size::Fill(1.0))
286 .height(Size::Fill(1.0))
287}
288
289fn chart_bars() -> Vec<El> {
290 [
291 48.0, 72.0, 56.0, 90.0, 64.0, 80.0, 108.0, 84.0, 122.0, 96.0, 136.0, 118.0,
292 ]
293 .into_iter()
294 .flat_map(|height| {
295 [
296 bar(height, tokens::MUTED_FOREGROUND),
297 bar((height - 28.0_f32).max(24.0), tokens::INPUT),
298 ]
299 })
300 .collect()
301}
302
303fn bar(height: f32, color: Color) -> El {
304 El::new(Kind::Custom("chart_bar"))
305 .fill(color)
306 .radius(tokens::RADIUS_SM)
307 .width(Size::Fill(1.0))
308 .height(Size::Fixed(height))
309}
310
311fn sales_card() -> El {
312 card([
313 card_header([
314 card_title("Recent Sales"),
315 card_description("You made 265 sales this month."),
316 ])
317 .padding(tokens::SPACE_4),
318 card_content([
319 sale_row("OM", "Olivia Martin", "olivia@example.com", "+$1,999.00"),
320 sale_row("JL", "Jackson Lee", "jackson@example.com", "+$39.00"),
321 sale_row("IN", "Isabella Nguyen", "isabella@example.com", "+$299.00"),
322 sale_row("WK", "William Kim", "will@example.com", "+$99.00"),
323 ])
324 .gap(tokens::SPACE_2)
325 .padding(Sides {
326 left: tokens::SPACE_4,
327 right: tokens::SPACE_4,
328 top: 0.0,
329 bottom: tokens::SPACE_4,
330 }),
331 ])
332 .key("metric:sales.card")
333 .width(Size::Fixed(330.0))
334 .height(Size::Fill(1.0))
335}
336
337fn sale_row(
338 initials: &'static str,
339 name: &'static str,
340 email: &'static str,
341 amount: &'static str,
342) -> El {
343 row([
344 icon_cell(initials),
345 column([
346 text(name).semibold().ellipsis().width(Size::Fill(1.0)),
347 text(email).caption().ellipsis().width(Size::Fill(1.0)),
348 ])
349 .gap(2.0)
350 .height(Size::Hug)
351 .width(Size::Fill(1.0)),
352 text(amount).label().small(),
353 ])
354 .gap(tokens::SPACE_2)
355 .height(Size::Fixed(42.0))
356 .align(Align::Center)
357}
358
359fn documents_card() -> El {
360 card([
361 card_header([card_title("Documents")]).padding(tokens::SPACE_4),
362 card_content([scroll([table([
363 table_header([table_row([
364 table_head("").width(Size::Fixed(35.0)),
365 table_head("Header").width(Size::Fill(1.8)),
366 table_head("Section Type").width(Size::Fill(1.0)),
367 table_head("Status").width(Size::Fixed(104.0)),
368 table_head("Target").width(Size::Fixed(64.0)),
369 table_head("Limit").width(Size::Fixed(64.0)),
370 table_head("Reviewer").width(Size::Fixed(128.0)),
371 table_head("").width(Size::Fixed(32.0)),
372 ])
373 .padding(Sides::xy(tokens::SPACE_4, 0.0))
374 .key("metric:table.header")]),
375 divider(),
376 table_body([
377 document_row(
378 "Cover page",
379 "Cover page",
380 "In Process",
381 "18",
382 "5",
383 "Eddie Lake",
384 "info",
385 ),
386 document_row(
387 "Table of contents",
388 "Table of contents",
389 "Done",
390 "29",
391 "24",
392 "Eddie Lake",
393 "success",
394 ),
395 ]),
396 ])])
397 .height(Size::Fill(1.0))])
398 .gap(0.0)
399 .padding(0.0)
400 .height(Size::Fill(1.0)),
401 ])
402 .key("metric:table.card")
403 .height(Size::Fill(1.0))
404}
405
406fn document_row(
407 header: &'static str,
408 section: &'static str,
409 status: &'static str,
410 target: &'static str,
411 limit: &'static str,
412 reviewer: &'static str,
413 tone: &'static str,
414) -> El {
415 let status_badge = match tone {
416 "success" => badge(status).success(),
417 _ => badge(status).info(),
418 };
419 table_row([
420 table_utility_cell("::"),
421 table_cell(text(header).label().small()).width(Size::Fill(1.8)),
422 table_cell(text(section).muted()).width(Size::Fill(1.0)),
423 table_cell(status_badge).width(Size::Fixed(104.0)),
424 table_cell(text(target).label().small()).width(Size::Fixed(64.0)),
425 table_cell(text(limit).label().small()).width(Size::Fixed(64.0)),
426 table_cell(text(reviewer).muted()).width(Size::Fixed(128.0)),
427 table_action_cell(),
428 ])
429 .padding(Sides::xy(tokens::SPACE_4, 0.0))
430 .key(if header == "Cover page" {
431 "metric:table.row"
432 } else {
433 header
434 })
435}
436
437fn table_utility_cell(label: &'static str) -> El {
438 table_cell(text(label).muted().center_text()).width(Size::Fixed(35.0))
439}
440
441fn table_action_cell() -> El {
442 stack([icon("more-horizontal")
443 .icon_size(tokens::ICON_SM)
444 .color(tokens::MUTED_FOREGROUND)])
445 .align(Align::Center)
446 .justify(Justify::Center)
447 .width(Size::Fixed(32.0))
448 .height(Size::Hug)
449}20fn list_rows() -> Vec<El> {
21 (0..40)
22 .map(|i| {
23 row([
24 text(format!("{i:02}.")).mono().muted(),
25 text(format!("scrollable list item {i}")),
26 ])
27 .gap(tokens::SPACE_2)
28 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_1))
29 .height(Size::Fixed(28.0))
30 .align(Align::Center)
31 })
32 .collect()
33}
34
35fn fixture() -> El {
36 column([
37 h2("Scrollbar"),
38 text("scroll() and virtual_list() show a draggable thumb by default.").muted(),
39 row([
40 // 1) scroll() — default-on scrollbar.
41 column([
42 text("scroll() — default").bold(),
43 scroll(list_rows())
44 .height(Size::Fixed(240.0))
45 .padding(tokens::SPACE_2)
46 .stroke(tokens::BORDER)
47 .stroke_width(1.0)
48 .radius(tokens::RADIUS_MD),
49 ])
50 .gap(tokens::SPACE_2)
51 .width(Size::Fill(1.0))
52 .height(Size::Hug),
53 // 2) virtual_list — thumb scales to content size.
54 column([
55 text("virtual_list(200, 28)").bold(),
56 virtual_list(200, 28.0, |i| {
57 row([
58 text(format!("{i:03}")).mono().muted(),
59 text(format!("row {i}")),
60 ])
61 .gap(tokens::SPACE_2)
62 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_1))
63 .height(Size::Fixed(28.0))
64 .align(Align::Center)
65 })
66 .height(Size::Fixed(240.0))
67 .padding(tokens::SPACE_2)
68 .stroke(tokens::BORDER)
69 .stroke_width(1.0)
70 .radius(tokens::RADIUS_MD),
71 ])
72 .gap(tokens::SPACE_2)
73 .width(Size::Fill(1.0))
74 .height(Size::Hug),
75 // 3) Opt-out: same content, no thumb.
76 column([
77 text("scroll().no_scrollbar()").bold(),
78 scroll(list_rows())
79 .no_scrollbar()
80 .height(Size::Fixed(240.0))
81 .padding(tokens::SPACE_2)
82 .stroke(tokens::BORDER)
83 .stroke_width(1.0)
84 .radius(tokens::RADIUS_MD),
85 ])
86 .gap(tokens::SPACE_2)
87 .width(Size::Fill(1.0))
88 .height(Size::Hug),
89 ])
90 .gap(tokens::SPACE_4)
91 .width(Size::Fill(1.0)),
92 ])
93 .gap(tokens::SPACE_4)
94 .padding(tokens::SPACE_7)
95}68fn build_row(c: &FakeCommit, idx: usize, selected: bool) -> El {
69 row([
70 graph_cell(c.lane, selected),
71 text(c.sha).mono().muted(),
72 text(c.subject),
73 spacer(),
74 text(format!("{} · {}", c.author, c.when)).muted(),
75 ])
76 .key(format!("commit-{idx}"))
77 .gap(tokens::SPACE_3)
78 .padding(Sides::xy(tokens::SPACE_2, 0.0))
79 .height(Size::Fixed(ROW_HEIGHT))
80 .align(Align::Center)
81}77fn palette_demo(label: &'static str, palette: &Palette) -> El {
78 column([
79 row([
80 column([
81 h1("Palette demo"),
82 text("Aetna and Radix palettes rendered through Aetna tokens.").muted(),
83 ])
84 .gap(tokens::SPACE_2)
85 .height(Size::Hug),
86 spacer(),
87 badge(label).muted(),
88 ])
89 .align(Align::Start)
90 .height(Size::Hug),
91 row([
92 token_section(
93 "Core tokens",
94 "The shadcn-shaped semantic vocabulary.",
95 &CORE_TOKENS,
96 palette,
97 )
98 .width(Size::Fill(1.25)),
99 column([
100 token_section(
101 "Aetna extensions",
102 "Component and status tokens layered on top.",
103 &EXTENSION_TOKENS,
104 palette,
105 ),
106 component_section(),
107 ])
108 .gap(tokens::SPACE_4)
109 .width(Size::Fill(1.0))
110 .height(Size::Fill(1.0)),
111 ])
112 .gap(tokens::SPACE_4)
113 .align(Align::Stretch)
114 .height(Size::Fill(1.0)),
115 ])
116 .padding(tokens::SPACE_8)
117 .gap(tokens::SPACE_6)
118 .fill_size()
119 .fill(tokens::BACKGROUND)
120}
121
122const CORE_TOKENS: [TokenDef; 19] = [
123 TokenDef {
124 name: "background",
125 color: tokens::BACKGROUND,
126 },
127 TokenDef {
128 name: "foreground",
129 color: tokens::FOREGROUND,
130 },
131 TokenDef {
132 name: "card",
133 color: tokens::CARD,
134 },
135 TokenDef {
136 name: "card-foreground",
137 color: tokens::CARD_FOREGROUND,
138 },
139 TokenDef {
140 name: "popover",
141 color: tokens::POPOVER,
142 },
143 TokenDef {
144 name: "popover-foreground",
145 color: tokens::POPOVER_FOREGROUND,
146 },
147 TokenDef {
148 name: "primary",
149 color: tokens::PRIMARY,
150 },
151 TokenDef {
152 name: "primary-foreground",
153 color: tokens::PRIMARY_FOREGROUND,
154 },
155 TokenDef {
156 name: "secondary",
157 color: tokens::SECONDARY,
158 },
159 TokenDef {
160 name: "secondary-foreground",
161 color: tokens::SECONDARY_FOREGROUND,
162 },
163 TokenDef {
164 name: "muted",
165 color: tokens::MUTED,
166 },
167 TokenDef {
168 name: "muted-foreground",
169 color: tokens::MUTED_FOREGROUND,
170 },
171 TokenDef {
172 name: "accent",
173 color: tokens::ACCENT,
174 },
175 TokenDef {
176 name: "accent-foreground",
177 color: tokens::ACCENT_FOREGROUND,
178 },
179 TokenDef {
180 name: "destructive",
181 color: tokens::DESTRUCTIVE,
182 },
183 TokenDef {
184 name: "destructive-foreground",
185 color: tokens::DESTRUCTIVE_FOREGROUND,
186 },
187 TokenDef {
188 name: "border",
189 color: tokens::BORDER,
190 },
191 TokenDef {
192 name: "input",
193 color: tokens::INPUT,
194 },
195 TokenDef {
196 name: "ring",
197 color: tokens::RING,
198 },
199];
200
201const EXTENSION_TOKENS: [TokenDef; 12] = [
202 TokenDef {
203 name: "success",
204 color: tokens::SUCCESS,
205 },
206 TokenDef {
207 name: "success-foreground",
208 color: tokens::SUCCESS_FOREGROUND,
209 },
210 TokenDef {
211 name: "warning",
212 color: tokens::WARNING,
213 },
214 TokenDef {
215 name: "warning-foreground",
216 color: tokens::WARNING_FOREGROUND,
217 },
218 TokenDef {
219 name: "info",
220 color: tokens::INFO,
221 },
222 TokenDef {
223 name: "info-foreground",
224 color: tokens::INFO_FOREGROUND,
225 },
226 TokenDef {
227 name: "link-foreground",
228 color: tokens::LINK_FOREGROUND,
229 },
230 TokenDef {
231 name: "overlay-scrim",
232 color: tokens::OVERLAY_SCRIM,
233 },
234 TokenDef {
235 name: "scrollbar-thumb",
236 color: tokens::SCROLLBAR_THUMB_FILL,
237 },
238 TokenDef {
239 name: "scrollbar-thumb-active",
240 color: tokens::SCROLLBAR_THUMB_FILL_ACTIVE,
241 },
242 TokenDef {
243 name: "selection-bg",
244 color: tokens::SELECTION_BG,
245 },
246 TokenDef {
247 name: "selection-bg-unfocused",
248 color: tokens::SELECTION_BG_UNFOCUSED,
249 },
250];
251
252fn token_section(
253 title: &'static str,
254 description: &'static str,
255 tokens: &'static [TokenDef],
256 palette: &Palette,
257) -> El {
258 card([
259 card_header([card_title(title), card_description(description)]),
260 card_content([swatch_grid(tokens, palette)]),
261 ])
262 .height(Size::Fill(1.0))
263}
264
265fn swatch_grid(defs: &'static [TokenDef], palette: &Palette) -> El {
266 let rows = defs
267 .chunks(2)
268 .map(|chunk| {
269 row(chunk.iter().map(|token| token_chip(*token, palette)))
270 .gap(tokens::SPACE_3)
271 .align(Align::Center)
272 .width(Size::Fill(1.0))
273 })
274 .collect::<Vec<_>>();
275
276 column(rows)
277 .gap(tokens::SPACE_3)
278 .width(Size::Fill(1.0))
279 .height(Size::Hug)
280}
281
282fn token_chip(token: TokenDef, palette: &Palette) -> El {
283 let resolved = palette.resolve(token.color);
284 row([
285 El::new(Kind::Custom("palette-swatch"))
286 .at(file!(), line!())
287 .fill(token.color)
288 .stroke(tokens::BORDER)
289 .radius(tokens::RADIUS_SM)
290 .width(Size::Fixed(42.0))
291 .height(Size::Fixed(34.0)),
292 column([
293 text(token.name)
294 .label()
295 .ellipsis()
296 .nowrap_text()
297 .width(Size::Fill(1.0)),
298 mono(rgba_label(resolved)).caption().muted(),
299 ])
300 .gap(0.0)
301 .width(Size::Fill(1.0))
302 .height(Size::Hug),
303 ])
304 .gap(tokens::SPACE_2)
305 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_2))
306 .align(Align::Center)
307 .fill(tokens::CARD)
308 .stroke(tokens::BORDER)
309 .radius(tokens::RADIUS_MD)
310 .width(Size::Fill(1.0))
311 .height(Size::Fixed(54.0))
312}
313
314fn component_section() -> El {
315 card([
316 card_header([
317 card_title("Stock widgets"),
318 card_description("The same palette applied to regular component constructors."),
319 ]),
320 card_content([
321 row([
322 button("Primary").primary(),
323 button("Secondary").secondary(),
324 button("Outline").outline(),
325 button("Ghost").ghost(),
326 ])
327 .gap(tokens::SPACE_2)
328 .align(Align::Center),
329 row([
330 badge("success").success(),
331 badge("warning").warning(),
332 badge("destructive").destructive(),
333 badge("info").info(),
334 badge("muted").muted(),
335 ])
336 .gap(tokens::SPACE_2)
337 .align(Align::Center),
338 row([
339 text_input("palette search", &Selection::default(), "palette:search")
340 .width(Size::Fill(1.0)),
341 button_with_icon("settings", "Tune").secondary(),
342 ])
343 .gap(tokens::SPACE_2)
344 .align(Align::Center),
345 row([
346 surface_sample("Card", tokens::CARD),
347 surface_sample("Muted", tokens::MUTED),
348 surface_sample("Popover", tokens::POPOVER),
349 ])
350 .gap(tokens::SPACE_3)
351 .align(Align::Stretch),
352 ])
353 .gap(tokens::SPACE_4),
354 ])
355 .height(Size::Hug)
356}Sourcepub fn justify(self, j: Justify) -> Self
pub fn justify(self, j: Justify) -> Self
Examples found in repository?
More examples
377fn icon_slot(icon_name: &'static str) -> El {
378 El::new(Kind::Custom("icon_cell"))
379 .style_profile(StyleProfile::Surface)
380 .child(
381 icon(icon_name)
382 .color(tokens::FOREGROUND)
383 .icon_size(tokens::ICON_XS),
384 )
385 .align(Align::Center)
386 .justify(Justify::Center)
387 .fill(tokens::MUTED)
388 .stroke(tokens::BORDER)
389 .radius(tokens::RADIUS_SM)
390 .width(Size::Fixed(30.0))
391 .height(Size::Fixed(30.0))
392}pub fn clip(self) -> Self
pub fn scrollable(self) -> Self
Sourcepub fn scrollbar(self) -> Self
pub fn scrollbar(self) -> Self
Show a draggable vertical scrollbar thumb when this scrollable node’s content overflows.
Sourcepub fn no_scrollbar(self) -> Self
pub fn no_scrollbar(self) -> Self
Suppress the default scrollbar thumb on this scrollable node.
Examples found in repository?
35fn fixture() -> El {
36 column([
37 h2("Scrollbar"),
38 text("scroll() and virtual_list() show a draggable thumb by default.").muted(),
39 row([
40 // 1) scroll() — default-on scrollbar.
41 column([
42 text("scroll() — default").bold(),
43 scroll(list_rows())
44 .height(Size::Fixed(240.0))
45 .padding(tokens::SPACE_2)
46 .stroke(tokens::BORDER)
47 .stroke_width(1.0)
48 .radius(tokens::RADIUS_MD),
49 ])
50 .gap(tokens::SPACE_2)
51 .width(Size::Fill(1.0))
52 .height(Size::Hug),
53 // 2) virtual_list — thumb scales to content size.
54 column([
55 text("virtual_list(200, 28)").bold(),
56 virtual_list(200, 28.0, |i| {
57 row([
58 text(format!("{i:03}")).mono().muted(),
59 text(format!("row {i}")),
60 ])
61 .gap(tokens::SPACE_2)
62 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_1))
63 .height(Size::Fixed(28.0))
64 .align(Align::Center)
65 })
66 .height(Size::Fixed(240.0))
67 .padding(tokens::SPACE_2)
68 .stroke(tokens::BORDER)
69 .stroke_width(1.0)
70 .radius(tokens::RADIUS_MD),
71 ])
72 .gap(tokens::SPACE_2)
73 .width(Size::Fill(1.0))
74 .height(Size::Hug),
75 // 3) Opt-out: same content, no thumb.
76 column([
77 text("scroll().no_scrollbar()").bold(),
78 scroll(list_rows())
79 .no_scrollbar()
80 .height(Size::Fixed(240.0))
81 .padding(tokens::SPACE_2)
82 .stroke(tokens::BORDER)
83 .stroke_width(1.0)
84 .radius(tokens::RADIUS_MD),
85 ])
86 .gap(tokens::SPACE_2)
87 .width(Size::Fill(1.0))
88 .height(Size::Hug),
89 ])
90 .gap(tokens::SPACE_4)
91 .width(Size::Fill(1.0)),
92 ])
93 .gap(tokens::SPACE_4)
94 .padding(tokens::SPACE_7)
95}Sourcepub fn pin_end(self) -> Self
pub fn pin_end(self) -> Self
Stick this scroll viewport’s offset to the tail of its content
the way chat logs and activity feeds do — when new children land
below the current bottom, the offset follows them; when the user
scrolls up, the pin releases; when the user scrolls back to the
bottom, it re-engages. Mirrors egui::ScrollArea::stick_to_bottom.
On first layout the offset starts at max_offset, so a freshly
mounted scroll([...]).pin_end() paints with its tail visible
rather than its head. Programmatic
crate::scroll::ScrollRequest::EnsureVisible requests that
resolve away from the tail also release the pin, so a
“jump-to-message N” action behaves as the user expects.
Sourcepub fn virtual_anchor_policy(self, policy: VirtualAnchorPolicy) -> Self
pub fn virtual_anchor_policy(self, policy: VirtualAnchorPolicy) -> Self
Override how a dynamic virtual list chooses the in-viewport row point that anchors the next frame.
Treat this element’s focusable children as a single arrow-navigable group.
Sourcepub fn layout<F>(self, f: F) -> Self
pub fn layout<F>(self, f: F) -> Self
Replace the column/row/overlay distribution for this node with a custom child layout function.
Examples found in repository?
48fn fixture() -> El {
49 let centre = h2("Compass").center_text();
50 let dirs = [
51 ("North", "n"),
52 ("NE", "ne"),
53 ("East", "e"),
54 ("SE", "se"),
55 ("South", "s"),
56 ("SW", "sw"),
57 ("West", "w"),
58 ("NW", "nw"),
59 ];
60
61 let mut children: Vec<El> = vec![centre];
62 for (label, k) in dirs {
63 children.push(button(label).key(k).primary());
64 }
65
66 column([
67 h1("Custom layout — circular"),
68 paragraph(
69 "Eight buttons positioned on a circle by an author-supplied \
70 LayoutFn. Stock paint, automatic hover/press, and hit-test \
71 all keep working — only the rect distribution changed.",
72 )
73 .muted(),
74 stack(children)
75 .key("compass")
76 .layout(circular)
77 .width(Size::Fill(1.0))
78 .height(Size::Fixed(360.0)),
79 ])
80 .gap(tokens::SPACE_4)
81 .padding(tokens::SPACE_7)
82}Sourcepub fn child(self, c: impl Into<El>) -> Self
pub fn child(self, c: impl Into<El>) -> Self
Examples found in repository?
377fn icon_slot(icon_name: &'static str) -> El {
378 El::new(Kind::Custom("icon_cell"))
379 .style_profile(StyleProfile::Surface)
380 .child(
381 icon(icon_name)
382 .color(tokens::FOREGROUND)
383 .icon_size(tokens::ICON_XS),
384 )
385 .align(Align::Center)
386 .justify(Justify::Center)
387 .fill(tokens::MUTED)
388 .stroke(tokens::BORDER)
389 .radius(tokens::RADIUS_SM)
390 .width(Size::Fixed(30.0))
391 .height(Size::Fixed(30.0))
392}pub fn children<I, E>(self, cs: I) -> Self
Source§impl El
impl El
Sourcepub fn fill(self, c: Color) -> Self
pub fn fill(self, c: Color) -> Self
Examples found in repository?
35fn polish_calibration() -> El {
36 row([sidebar(), main_panel()])
37 .key("metric:root")
38 .gap(0.0)
39 .fill_size()
40 .align(Align::Stretch)
41 .fill(tokens::BACKGROUND)
42}
43
44fn sidebar() -> El {
45 column([
46 column([h2("Aetna"), text("calibration").muted()])
47 .key("metric:sidebar.brand")
48 .gap(tokens::SPACE_1)
49 .height(Size::Hug),
50 spacer().height(Size::Fixed(tokens::SPACE_4)),
51 nav_item("01", "Overview", true),
52 nav_item("02", "Commands", false),
53 nav_item("03", "Tables", false),
54 nav_item("04", "Forms", false),
55 spacer(),
56 badge("dark theme").muted(),
57 ])
58 .gap(tokens::SPACE_2)
59 .padding(tokens::SPACE_5)
60 .key("metric:sidebar")
61 .width(Size::Fixed(220.0))
62 .height(Size::Fill(1.0))
63 .fill(tokens::CARD)
64 .stroke(tokens::BORDER)
65}
66
67fn nav_item(icon: &'static str, label: &'static str, selected: bool) -> El {
68 let mut item = row([
69 icon_cell(icon),
70 text(label)
71 .font_weight(FontWeight::Medium)
72 .ellipsis()
73 .width(Size::Fill(1.0)),
74 ])
75 .key(if selected {
76 "metric:sidebar.nav.row".to_string()
77 } else {
78 format!("nav-{label}")
79 })
80 .metrics_role(MetricsRole::ListItem)
81 .gap(tokens::SPACE_3)
82 .padding(Sides::xy(tokens::SPACE_2, 0.0))
83 .height(Size::Fixed(40.0))
84 .align(Align::Center)
85 .focusable();
86
87 if selected {
88 item = item.current();
89 }
90
91 item
92}
93
94fn main_panel() -> El {
95 column([
96 toolbar(),
97 column([
98 row([
99 kpi_card("Latency", "42 ms", "-18%", true),
100 kpi_card("Runs", "1,284", "+12%", true),
101 kpi_card("Errors", "7", "+2", false),
102 ])
103 .gap(tokens::SPACE_4),
104 row([table_card(), command_card()])
105 .gap(tokens::SPACE_4)
106 .height(Size::Fill(1.0))
107 .align(Align::Stretch),
108 ])
109 .gap(tokens::SPACE_4)
110 .height(Size::Fill(1.0))
111 .align(Align::Stretch),
112 ])
113 .padding(tokens::SPACE_7)
114 .gap(tokens::SPACE_2)
115 .width(Size::Fill(1.0))
116 .height(Size::Fill(1.0))
117}
118
119fn toolbar() -> El {
120 row([
121 column([
122 h1("Polish calibration").key("metric:page.title"),
123 text("A representative app surface for default tuning.")
124 .muted()
125 .key("metric:page.subtitle"),
126 ])
127 .gap(tokens::SPACE_2)
128 .height(Size::Hug),
129 spacer(),
130 button_with_icon("search", "Preview")
131 .secondary()
132 .key("metric:action.secondary"),
133 button_with_icon("upload", "Publish")
134 .primary()
135 .key("metric:action.primary"),
136 ])
137 .key("metric:header")
138 .gap(tokens::SPACE_4)
139 .height(Size::Hug)
140 .align(Align::Start)
141}
142
143fn kpi_card(label: &'static str, value: &'static str, delta: &'static str, positive: bool) -> El {
144 let delta_badge = if positive {
145 badge(delta).success()
146 } else {
147 badge(delta).destructive()
148 };
149 let delta_badge = if label == "Latency" {
150 delta_badge.key("metric:kpi.badge")
151 } else {
152 delta_badge
153 };
154 let value_text = h2(value).display();
155 let value_text = if label == "Latency" {
156 value_text.key("metric:kpi.value")
157 } else {
158 value_text
159 };
160 card([
161 card_header([card_title(label)]),
162 card_content([
163 row([value_text, spacer(), delta_badge]).align(Align::Center),
164 text(if positive {
165 "Moving in the expected direction"
166 } else {
167 "Needs visual attention"
168 })
169 .muted(),
170 ])
171 .gap(tokens::SPACE_6),
172 ])
173 .key(if label == "Latency" {
174 "metric:kpi.card"
175 } else {
176 label
177 })
178 .width(Size::Fill(1.0))
179}
180
181fn table_card() -> El {
182 card([
183 card_header([card_title("Reference rows")]),
184 card_content([table([
185 table_header([table_row([
186 table_head("Status").width(Size::Fixed(86.0)),
187 table_head("Surface").width(Size::Fill(1.0)),
188 table_head("Owner").width(Size::Fixed(110.0)),
189 table_head("State").width(Size::Fixed(86.0)),
190 ])
191 .key("metric:table.header")]),
192 divider(),
193 table_body([
194 data_row("OK", "Settings card", "core", "selected", true, "success"),
195 data_row(
196 "WARN",
197 "Command palette density",
198 "widgets",
199 "needs work",
200 false,
201 "warning",
202 ),
203 data_row(
204 "ERR",
205 "Disabled and invalid states",
206 "style",
207 "missing",
208 false,
209 "destructive",
210 ),
211 data_row(
212 "INFO",
213 "Token resolution",
214 "theme",
215 "planned",
216 false,
217 "info",
218 ),
219 data_row(
220 "OK",
221 "Popover elevation",
222 "shader",
223 "queued",
224 false,
225 "success",
226 ),
227 ])
228 .gap(tokens::SPACE_1)
229 .width(Size::Fill(1.0)),
230 ])]),
231 ])
232 .key("metric:table.card")
233 .width(Size::Fill(1.2))
234 .height(Size::Fill(1.0))
235}
236
237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}
287
288fn command_card() -> El {
289 card([
290 card_header([card_title("Command surface")]),
291 card_content([
292 text_input(
293 "Search commands...",
294 &Selection::default(),
295 "command-search",
296 )
297 .key("metric:command.input")
298 .width(Size::Fill(1.0)),
299 popover_panel([
300 command_row("git-branch", "New branch", "Ctrl+B").key("metric:command.row"),
301 command_row("git-commit", "Commit staged files", "Ctrl+Enter")
302 .key("command-row-commit"),
303 command_row("refresh-cw", "Refresh repository", "Ctrl+R")
304 .key("command-row-refresh"),
305 command_row("alert-circle", "Force push", "Danger").key("command-row-force"),
306 ])
307 .width(Size::Fill(1.0)),
308 scroll([form_probe()]).key("form-probe-scroll"),
309 ])
310 .height(Size::Fill(1.0)),
311 ])
312 .key("metric:command.card")
313 .width(Size::Fill(0.8))
314 .height(Size::Fill(1.0))
315}
316
317fn form_probe() -> El {
318 form([
319 form_item([
320 form_label("Valid input"),
321 form_control(
322 text_input(
323 "Valid input",
324 &Selection::caret("valid-input", 11),
325 "valid-input",
326 )
327 .key("metric:form.input"),
328 ),
329 form_description("Default field spacing and helper text."),
330 ]),
331 form_item([
332 form_label("Invalid input"),
333 form_control(
334 text_input(
335 "Invalid input",
336 &Selection::caret("invalid-input", 13),
337 "invalid-input",
338 )
339 .invalid(),
340 ),
341 form_message("This field needs attention."),
342 ]),
343 row([
344 button("Disabled").secondary().disabled(),
345 button("Loading").primary().loading(),
346 spacer(),
347 ]),
348 ])
349 .padding(tokens::SPACE_3)
350 .fill(tokens::MUTED)
351 .stroke(tokens::BORDER)
352 .radius(tokens::RADIUS_MD)
353}
354
355fn icon_cell(label: &'static str) -> El {
356 El::new(Kind::Custom("icon_cell"))
357 .style_profile(StyleProfile::Surface)
358 .text(label)
359 .text_align(TextAlign::Center)
360 .caption()
361 .font_weight(FontWeight::Semibold)
362 .fill(tokens::MUTED)
363 .stroke(tokens::BORDER)
364 .radius(tokens::RADIUS_SM)
365 .width(Size::Fixed(26.0))
366 .height(Size::Fixed(26.0))
367}More examples
32fn settings_calibration() -> El {
33 row([settings_sidebar(), settings_main()])
34 .key("metric:root")
35 .gap(0.0)
36 .fill_size()
37 .align(Align::Stretch)
38 .fill(tokens::BACKGROUND)
39}
40
41fn settings_sidebar() -> El {
42 column([
43 row([
44 icon_slot("settings"),
45 column([
46 text("Workspace")
47 .semibold()
48 .ellipsis()
49 .width(Size::Fill(1.0)),
50 text("Settings").caption().ellipsis().width(Size::Fill(1.0)),
51 ])
52 .gap(2.0)
53 .width(Size::Fill(1.0))
54 .height(Size::Hug),
55 ])
56 .gap(tokens::SPACE_2)
57 .height(Size::Fixed(44.0))
58 .align(Align::Center),
59 section_label("Personal"),
60 side_item("users", "Profile", false),
61 side_item("settings", "Account", true),
62 side_item("alert-circle", "Security", false),
63 side_item("bell", "Notifications", false),
64 spacer().height(Size::Fixed(tokens::SPACE_4)),
65 section_label("Workspace"),
66 side_item("file-text", "Billing", false),
67 side_item("bar-chart", "Appearance", false),
68 side_item("activity", "Integrations", false),
69 spacer(),
70 column([text("Changes sync after save.").caption().wrap_text()])
71 .padding(tokens::SPACE_2)
72 .fill(tokens::MUTED)
73 .radius(tokens::RADIUS_MD),
74 ])
75 .gap(tokens::SPACE_2)
76 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
77 .key("metric:sidebar")
78 .width(Size::Fixed(244.0))
79 .height(Size::Fill(1.0))
80 .fill(tokens::CARD)
81 .stroke(tokens::BORDER)
82}
83
84fn settings_main() -> El {
85 column([
86 settings_header(),
87 row([settings_nav_card(), settings_body(), settings_aside()])
88 .gap(tokens::SPACE_4)
89 .padding(tokens::SPACE_4)
90 .height(Size::Fill(1.0))
91 .align(Align::Stretch),
92 ])
93 .width(Size::Fill(1.0))
94 .height(Size::Fill(1.0))
95}
96
97fn settings_header() -> El {
98 row([
99 icon_button("menu").ghost(),
100 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
101 h3("Settings").key("metric:page.title"),
102 spacer(),
103 button("Reset").secondary(),
104 button("Save changes").primary(),
105 ])
106 .key("metric:header")
107 .gap(tokens::SPACE_3)
108 .height(Size::Fixed(56.0))
109 .padding(Sides::xy(tokens::SPACE_4, 0.0))
110 .align(Align::Center)
111 .stroke(tokens::BORDER)
112}
113
114fn settings_nav_card() -> El {
115 column([
116 settings_nav_item("Account", true),
117 settings_nav_item("Security", false),
118 settings_nav_item("Notifications", false),
119 settings_nav_item("Appearance", false),
120 settings_nav_item("Billing", false),
121 ])
122 .gap(tokens::SPACE_1)
123 .padding(tokens::SPACE_1)
124 .width(Size::Fixed(220.0))
125 .height(Size::Fill(1.0))
126 .style_profile(StyleProfile::Surface)
127 .surface_role(SurfaceRole::Panel)
128 .fill(tokens::CARD)
129 .stroke(tokens::BORDER)
130 .radius(tokens::RADIUS_MD)
131 .shadow(tokens::SHADOW_MD)
132}
133
134fn settings_nav_item(label: &'static str, selected: bool) -> El {
135 let mut item = row([
136 El::new(Kind::Custom("nav-dot"))
137 .fill(tokens::MUTED_FOREGROUND)
138 .radius(tokens::RADIUS_PILL)
139 .width(Size::Fixed(6.0))
140 .height(Size::Fixed(6.0)),
141 text(label)
142 .font_weight(FontWeight::Medium)
143 .ellipsis()
144 .width(Size::Fill(1.0)),
145 ])
146 .key(if selected {
147 "metric:settings.nav.row".to_string()
148 } else {
149 format!("settings-nav-{label}")
150 })
151 .metrics_role(MetricsRole::ListItem)
152 .align(Align::Center)
153 .focusable();
154
155 if selected {
156 item = item.current();
157 } else {
158 item = item.color(tokens::MUTED_FOREGROUND);
159 }
160
161 item
162}
163
164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}
185
186fn profile_card() -> El {
187 card([
188 card_header([
189 card_title("Profile"),
190 card_description("This information appears in audit logs and shared documents."),
191 ]),
192 card_content([form([
193 row([
194 setting_field("Display name", "Alicia Koch", "display-name"),
195 setting_field("Email", "alicia@acme.co", "email"),
196 ])
197 .gap(tokens::SPACE_3),
198 row([
199 setting_select("Role", "Workspace admin", "role"),
200 setting_select("Region", "US East", "region"),
201 ])
202 .gap(tokens::SPACE_3),
203 ])]),
204 ])
205 .key("metric:profile.card")
206}
207
208fn setting_field(label: &'static str, value: &'static str, key: &'static str) -> El {
209 form_item([
210 form_label(label),
211 form_control(
212 text_input(value, &Selection::caret(key, value.len()), key).key(
213 if key == "display-name" {
214 "metric:form.input"
215 } else {
216 key
217 },
218 ),
219 ),
220 ])
221 .width(Size::Fill(1.0))
222}
223
224fn setting_select(label: &'static str, value: &'static str, key: &'static str) -> El {
225 form_item([form_label(label), form_control(select_trigger(key, value))]).width(Size::Fill(1.0))
226}
227
228fn preferences_card() -> El {
229 card([
230 card_header([
231 card_title("Preferences"),
232 card_description("Defaults used when creating new dashboards and exports."),
233 ]),
234 card_content([column([
235 preference_row(
236 "Compact navigation",
237 "Use tighter rows in the sidebar and command menus.",
238 switch(true).key("compact-navigation"),
239 ),
240 divider(),
241 preference_row(
242 "Email summaries",
243 "Send a daily digest when documents change.",
244 switch(false).key("email-summaries"),
245 ),
246 divider(),
247 preference_row(
248 "Require approval",
249 "Route external sharing through an owner review.",
250 checkbox(true).key("approval-required"),
251 ),
252 ])
253 .gap(0.0)
254 .width(Size::Fill(1.0))])
255 .padding(0.0),
256 ])
257 .key("metric:preferences.card")
258}
259
260fn preference_row(title: &'static str, description: &'static str, control: El) -> El {
261 row([
262 column([
263 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
264 text(description)
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0))
271 .height(Size::Hug),
272 control,
273 ])
274 .key(if title == "Compact navigation" {
275 "metric:preference.row".to_string()
276 } else {
277 format!("preference-{title}")
278 })
279 .metrics_role(MetricsRole::PreferenceRow)
280 .gap(tokens::SPACE_4)
281 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_3))
282 .align(Align::Center)
283}
284
285fn settings_aside() -> El {
286 column([security_card(), scale_card()])
287 .gap(tokens::SPACE_4)
288 .width(Size::Fixed(300.0))
289 .height(Size::Fill(1.0))
290}
291
292fn security_card() -> El {
293 card([
294 card_header([
295 card_title("Security"),
296 card_description("Two-factor authentication is enabled for all privileged users."),
297 ]),
298 card_content([
299 compact_stat("Passkeys", "2 registered", badge("On").success()),
300 compact_stat("Sessions", "3 active", button("Review").secondary()),
301 ]),
302 ])
303 .width(Size::Fill(1.0))
304}
305
306fn scale_card() -> El {
307 card([
308 card_header([
309 card_title("Interface scale"),
310 card_description("Reference captures keep browser zoom fixed and vary root UI scale."),
311 ]),
312 card_content([
313 row([text("Dense").caption(), spacer(), text("Default").caption()]),
314 slider(0.66, tokens::PRIMARY)
315 .key("interface-scale")
316 .width(Size::Fill(1.0)),
317 ]),
318 ])
319 .width(Size::Fill(1.0))
320}
321
322fn compact_stat(title: &'static str, detail: &'static str, control: El) -> El {
323 row([
324 column([
325 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
326 text(detail).caption().ellipsis().width(Size::Fill(1.0)),
327 ])
328 .gap(2.0)
329 .width(Size::Fill(1.0))
330 .height(Size::Hug),
331 control,
332 ])
333 .gap(tokens::SPACE_2)
334 .height(Size::Fixed(44.0))
335 .align(Align::Center)
336}
337
338fn section_label(label: &'static str) -> El {
339 text(label)
340 .caption()
341 .height(Size::Fixed(22.0))
342 .padding(Sides::xy(tokens::SPACE_2, 0.0))
343}
344
345fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
346 let mut item = row([
347 icon(icon_name)
348 .color(tokens::MUTED_FOREGROUND)
349 .icon_size(tokens::ICON_SM)
350 .width(Size::Fixed(tokens::ICON_SM)),
351 text(label)
352 .font_weight(FontWeight::Medium)
353 .ellipsis()
354 .width(Size::Fill(1.0)),
355 ])
356 .key(if selected {
357 "metric:sidebar.nav.row".to_string()
358 } else {
359 format!("side-item-{label}")
360 })
361 .metrics_role(MetricsRole::ListItem)
362 .gap(tokens::SPACE_2)
363 .padding(Sides::xy(tokens::SPACE_2, 0.0))
364 .height(Size::Fixed(32.0))
365 .align(Align::Center)
366 .focusable();
367
368 if selected {
369 item = item.current();
370 } else {
371 item = item.color(tokens::MUTED_FOREGROUND);
372 }
373
374 item
375}
376
377fn icon_slot(icon_name: &'static str) -> El {
378 El::new(Kind::Custom("icon_cell"))
379 .style_profile(StyleProfile::Surface)
380 .child(
381 icon(icon_name)
382 .color(tokens::FOREGROUND)
383 .icon_size(tokens::ICON_XS),
384 )
385 .align(Align::Center)
386 .justify(Justify::Center)
387 .fill(tokens::MUTED)
388 .stroke(tokens::BORDER)
389 .radius(tokens::RADIUS_SM)
390 .width(Size::Fixed(30.0))
391 .height(Size::Fixed(30.0))
392}32fn dashboard_01_calibration() -> El {
33 row([dashboard_sidebar(), dashboard_main()])
34 .key("metric:root")
35 .gap(0.0)
36 .fill_size()
37 .align(Align::Stretch)
38 .fill(tokens::BACKGROUND)
39}
40
41fn dashboard_sidebar() -> El {
42 column([
43 row([
44 icon_cell("A"),
45 column([
46 text("Acme Inc.")
47 .semibold()
48 .ellipsis()
49 .width(Size::Fill(1.0)),
50 text("Enterprise")
51 .caption()
52 .ellipsis()
53 .width(Size::Fill(1.0)),
54 ])
55 .gap(2.0)
56 .width(Size::Fill(1.0))
57 .height(Size::Hug),
58 ])
59 .gap(tokens::SPACE_2)
60 .height(Size::Fixed(44.0))
61 .align(Align::Center),
62 section_label("Platform"),
63 side_item("layout-dashboard", "Dashboard", true),
64 side_item("activity", "Lifecycle", false),
65 side_item("bar-chart", "Analytics", false),
66 side_item("folder", "Projects", false),
67 spacer().height(Size::Fixed(tokens::SPACE_4)),
68 section_label("Documents"),
69 side_item("file-text", "Data library", false),
70 side_item("download", "Reports", false),
71 side_item("users", "Team", false),
72 spacer(),
73 row([
74 icon_cell("AK"),
75 column([
76 text("Alicia Koch")
77 .semibold()
78 .ellipsis()
79 .width(Size::Fill(1.0)),
80 text("alicia@example.com")
81 .caption()
82 .ellipsis()
83 .width(Size::Fill(1.0)),
84 ])
85 .gap(2.0)
86 .width(Size::Fill(1.0))
87 .height(Size::Hug),
88 ])
89 .gap(tokens::SPACE_2)
90 .height(Size::Fixed(50.0))
91 .align(Align::Center),
92 ])
93 .gap(tokens::SPACE_2)
94 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
95 .key("metric:sidebar")
96 .width(Size::Fixed(244.0))
97 .height(Size::Fill(1.0))
98 .fill(tokens::CARD)
99 .stroke(tokens::BORDER)
100}
101
102fn section_label(label: &'static str) -> El {
103 text(label)
104 .caption()
105 .height(Size::Fixed(22.0))
106 .padding(Sides::xy(tokens::SPACE_2, 0.0))
107}
108
109fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
110 let mut item = row([
111 icon(icon_name)
112 .color(tokens::MUTED_FOREGROUND)
113 .icon_size(tokens::ICON_SM)
114 .width(Size::Fixed(tokens::ICON_SM)),
115 text(label)
116 .font_weight(FontWeight::Medium)
117 .ellipsis()
118 .width(Size::Fill(1.0)),
119 ])
120 .key(if selected {
121 "metric:sidebar.nav.row".to_string()
122 } else {
123 format!("side-item-{label}")
124 })
125 .metrics_role(MetricsRole::ListItem)
126 .gap(tokens::SPACE_2)
127 .padding(Sides::xy(tokens::SPACE_2, 0.0))
128 .height(Size::Fixed(32.0))
129 .align(Align::Center)
130 .focusable();
131
132 if selected {
133 item = item.current();
134 } else {
135 item = item.color(tokens::MUTED_FOREGROUND);
136 }
137
138 item
139}
140
141fn dashboard_main() -> El {
142 column([
143 dashboard_header(),
144 column([
145 row([
146 metric_card(
147 "bar-chart",
148 "Total Revenue",
149 "$1,250.00",
150 "+12.5%",
151 "Trending up this month",
152 true,
153 ),
154 metric_card(
155 "users",
156 "New Customers",
157 "1,234",
158 "-20%",
159 "Acquisition needs attention",
160 false,
161 ),
162 metric_card(
163 "folder",
164 "Active Accounts",
165 "45,678",
166 "+12.5%",
167 "Strong user retention",
168 true,
169 ),
170 metric_card(
171 "activity",
172 "Growth Rate",
173 "4.5%",
174 "+4.5%",
175 "Meets growth projections",
176 true,
177 ),
178 ])
179 .gap(tokens::SPACE_4),
180 row([chart_card(), sales_card()])
181 .gap(tokens::SPACE_4)
182 .height(Size::Fixed(306.0))
183 .align(Align::Stretch),
184 documents_card(),
185 ])
186 .gap(tokens::SPACE_4)
187 .padding(tokens::SPACE_7)
188 .height(Size::Fill(1.0)),
189 ])
190 .width(Size::Fill(1.0))
191 .height(Size::Fill(1.0))
192}
193
194fn dashboard_header() -> El {
195 row([
196 icon_button("menu").ghost(),
197 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
198 h3("Documents").key("metric:page.title"),
199 spacer(),
200 text_input("Search...", &Selection::default(), "dashboard-search")
201 .key("metric:command.input")
202 .width(Size::Fixed(260.0)),
203 icon_button("plus").ghost(),
204 icon_button("bell").ghost(),
205 ])
206 .key("metric:header")
207 .gap(tokens::SPACE_3)
208 .height(Size::Fixed(56.0))
209 .padding(Sides::xy(tokens::SPACE_4, 0.0))
210 .align(Align::Center)
211 .stroke(tokens::BORDER)
212}
213
214fn metric_card(
215 icon_name: &'static str,
216 title: &'static str,
217 value: &'static str,
218 delta: &'static str,
219 note: &'static str,
220 positive: bool,
221) -> El {
222 let badge = if positive {
223 badge(delta).success()
224 } else {
225 badge(delta).warning()
226 };
227 let badge = if title == "Total Revenue" {
228 badge.key("metric:kpi.badge")
229 } else {
230 badge
231 };
232 let value = if title == "Total Revenue" {
233 h2(value).ellipsis().key("metric:kpi.value")
234 } else {
235 h2(value).ellipsis()
236 };
237 card([card_content([
238 row([
239 row([
240 icon(icon_name)
241 .color(tokens::MUTED_FOREGROUND)
242 .icon_size(tokens::ICON_XS),
243 text(title).muted().ellipsis().width(Size::Fill(1.0)),
244 ])
245 .gap(tokens::SPACE_1)
246 .width(Size::Fill(1.0))
247 .align(Align::Center),
248 badge,
249 ])
250 .gap(tokens::SPACE_2)
251 .align(Align::Center),
252 value,
253 text(note).caption().ellipsis().width(Size::Fill(1.0)),
254 ])
255 .padding(tokens::SPACE_4)
256 .gap(tokens::SPACE_2)])
257 .key(if title == "Total Revenue" {
258 "metric:kpi.card"
259 } else {
260 title
261 })
262 .width(Size::Fill(1.0))
263}
264
265fn chart_card() -> El {
266 card([
267 card_header([
268 card_title("Visitors for the last 6 months"),
269 card_description("Total visitors by channel."),
270 ])
271 .padding(tokens::SPACE_4),
272 card_content([row(chart_bars())
273 .gap(2.0)
274 .height(Size::Fill(1.0))
275 .align(Align::End)])
276 .padding(Sides {
277 left: tokens::SPACE_4,
278 right: tokens::SPACE_4,
279 top: 0.0,
280 bottom: tokens::SPACE_4,
281 })
282 .height(Size::Fill(1.0)),
283 ])
284 .key("metric:chart.card")
285 .width(Size::Fill(1.0))
286 .height(Size::Fill(1.0))
287}
288
289fn chart_bars() -> Vec<El> {
290 [
291 48.0, 72.0, 56.0, 90.0, 64.0, 80.0, 108.0, 84.0, 122.0, 96.0, 136.0, 118.0,
292 ]
293 .into_iter()
294 .flat_map(|height| {
295 [
296 bar(height, tokens::MUTED_FOREGROUND),
297 bar((height - 28.0_f32).max(24.0), tokens::INPUT),
298 ]
299 })
300 .collect()
301}
302
303fn bar(height: f32, color: Color) -> El {
304 El::new(Kind::Custom("chart_bar"))
305 .fill(color)
306 .radius(tokens::RADIUS_SM)
307 .width(Size::Fill(1.0))
308 .height(Size::Fixed(height))
309}
310
311fn sales_card() -> El {
312 card([
313 card_header([
314 card_title("Recent Sales"),
315 card_description("You made 265 sales this month."),
316 ])
317 .padding(tokens::SPACE_4),
318 card_content([
319 sale_row("OM", "Olivia Martin", "olivia@example.com", "+$1,999.00"),
320 sale_row("JL", "Jackson Lee", "jackson@example.com", "+$39.00"),
321 sale_row("IN", "Isabella Nguyen", "isabella@example.com", "+$299.00"),
322 sale_row("WK", "William Kim", "will@example.com", "+$99.00"),
323 ])
324 .gap(tokens::SPACE_2)
325 .padding(Sides {
326 left: tokens::SPACE_4,
327 right: tokens::SPACE_4,
328 top: 0.0,
329 bottom: tokens::SPACE_4,
330 }),
331 ])
332 .key("metric:sales.card")
333 .width(Size::Fixed(330.0))
334 .height(Size::Fill(1.0))
335}
336
337fn sale_row(
338 initials: &'static str,
339 name: &'static str,
340 email: &'static str,
341 amount: &'static str,
342) -> El {
343 row([
344 icon_cell(initials),
345 column([
346 text(name).semibold().ellipsis().width(Size::Fill(1.0)),
347 text(email).caption().ellipsis().width(Size::Fill(1.0)),
348 ])
349 .gap(2.0)
350 .height(Size::Hug)
351 .width(Size::Fill(1.0)),
352 text(amount).label().small(),
353 ])
354 .gap(tokens::SPACE_2)
355 .height(Size::Fixed(42.0))
356 .align(Align::Center)
357}
358
359fn documents_card() -> El {
360 card([
361 card_header([card_title("Documents")]).padding(tokens::SPACE_4),
362 card_content([scroll([table([
363 table_header([table_row([
364 table_head("").width(Size::Fixed(35.0)),
365 table_head("Header").width(Size::Fill(1.8)),
366 table_head("Section Type").width(Size::Fill(1.0)),
367 table_head("Status").width(Size::Fixed(104.0)),
368 table_head("Target").width(Size::Fixed(64.0)),
369 table_head("Limit").width(Size::Fixed(64.0)),
370 table_head("Reviewer").width(Size::Fixed(128.0)),
371 table_head("").width(Size::Fixed(32.0)),
372 ])
373 .padding(Sides::xy(tokens::SPACE_4, 0.0))
374 .key("metric:table.header")]),
375 divider(),
376 table_body([
377 document_row(
378 "Cover page",
379 "Cover page",
380 "In Process",
381 "18",
382 "5",
383 "Eddie Lake",
384 "info",
385 ),
386 document_row(
387 "Table of contents",
388 "Table of contents",
389 "Done",
390 "29",
391 "24",
392 "Eddie Lake",
393 "success",
394 ),
395 ]),
396 ])])
397 .height(Size::Fill(1.0))])
398 .gap(0.0)
399 .padding(0.0)
400 .height(Size::Fill(1.0)),
401 ])
402 .key("metric:table.card")
403 .height(Size::Fill(1.0))
404}
405
406fn document_row(
407 header: &'static str,
408 section: &'static str,
409 status: &'static str,
410 target: &'static str,
411 limit: &'static str,
412 reviewer: &'static str,
413 tone: &'static str,
414) -> El {
415 let status_badge = match tone {
416 "success" => badge(status).success(),
417 _ => badge(status).info(),
418 };
419 table_row([
420 table_utility_cell("::"),
421 table_cell(text(header).label().small()).width(Size::Fill(1.8)),
422 table_cell(text(section).muted()).width(Size::Fill(1.0)),
423 table_cell(status_badge).width(Size::Fixed(104.0)),
424 table_cell(text(target).label().small()).width(Size::Fixed(64.0)),
425 table_cell(text(limit).label().small()).width(Size::Fixed(64.0)),
426 table_cell(text(reviewer).muted()).width(Size::Fixed(128.0)),
427 table_action_cell(),
428 ])
429 .padding(Sides::xy(tokens::SPACE_4, 0.0))
430 .key(if header == "Cover page" {
431 "metric:table.row"
432 } else {
433 header
434 })
435}
436
437fn table_utility_cell(label: &'static str) -> El {
438 table_cell(text(label).muted().center_text()).width(Size::Fixed(35.0))
439}
440
441fn table_action_cell() -> El {
442 stack([icon("more-horizontal")
443 .icon_size(tokens::ICON_SM)
444 .color(tokens::MUTED_FOREGROUND)])
445 .align(Align::Center)
446 .justify(Justify::Center)
447 .width(Size::Fixed(32.0))
448 .height(Size::Hug)
449}
450
451fn icon_cell(label: &'static str) -> El {
452 El::new(Kind::Custom("icon_cell"))
453 .style_profile(StyleProfile::Surface)
454 .text(label)
455 .text_align(TextAlign::Center)
456 .caption()
457 .font_weight(FontWeight::Semibold)
458 .fill(tokens::MUTED)
459 .radius(tokens::RADIUS_SM)
460 .width(Size::Fixed(30.0))
461 .height(Size::Fixed(30.0))
462}44fn graph_cell(lane: u8, selected: bool) -> El {
45 let lane_color = lane_palette(lane);
46 let ring_color = if selected {
47 Color::rgb(245, 245, 250)
48 } else {
49 lane_color
50 };
51 let ring_w = if selected { 2.5 } else { 1.5 };
52 let radius = 5.0;
53 let line_w = 2.0;
54 let lane_frac = (lane as f32 + 0.5) / LANE_COUNT as f32;
55
56 El::new(Kind::Custom("graph_cell"))
57 .width(Size::Fixed(GRAPH_WIDTH))
58 .height(Size::Fixed(ROW_HEIGHT))
59 .shader(
60 ShaderBinding::custom("commit_node")
61 .color("vec_a", tokens::BACKGROUND)
62 .color("vec_b", ring_color)
63 .vec4("vec_c", [radius, ring_w, line_w, lane_frac]),
64 )
65 .fill(lane_color)
66}77fn palette_demo(label: &'static str, palette: &Palette) -> El {
78 column([
79 row([
80 column([
81 h1("Palette demo"),
82 text("Aetna and Radix palettes rendered through Aetna tokens.").muted(),
83 ])
84 .gap(tokens::SPACE_2)
85 .height(Size::Hug),
86 spacer(),
87 badge(label).muted(),
88 ])
89 .align(Align::Start)
90 .height(Size::Hug),
91 row([
92 token_section(
93 "Core tokens",
94 "The shadcn-shaped semantic vocabulary.",
95 &CORE_TOKENS,
96 palette,
97 )
98 .width(Size::Fill(1.25)),
99 column([
100 token_section(
101 "Aetna extensions",
102 "Component and status tokens layered on top.",
103 &EXTENSION_TOKENS,
104 palette,
105 ),
106 component_section(),
107 ])
108 .gap(tokens::SPACE_4)
109 .width(Size::Fill(1.0))
110 .height(Size::Fill(1.0)),
111 ])
112 .gap(tokens::SPACE_4)
113 .align(Align::Stretch)
114 .height(Size::Fill(1.0)),
115 ])
116 .padding(tokens::SPACE_8)
117 .gap(tokens::SPACE_6)
118 .fill_size()
119 .fill(tokens::BACKGROUND)
120}
121
122const CORE_TOKENS: [TokenDef; 19] = [
123 TokenDef {
124 name: "background",
125 color: tokens::BACKGROUND,
126 },
127 TokenDef {
128 name: "foreground",
129 color: tokens::FOREGROUND,
130 },
131 TokenDef {
132 name: "card",
133 color: tokens::CARD,
134 },
135 TokenDef {
136 name: "card-foreground",
137 color: tokens::CARD_FOREGROUND,
138 },
139 TokenDef {
140 name: "popover",
141 color: tokens::POPOVER,
142 },
143 TokenDef {
144 name: "popover-foreground",
145 color: tokens::POPOVER_FOREGROUND,
146 },
147 TokenDef {
148 name: "primary",
149 color: tokens::PRIMARY,
150 },
151 TokenDef {
152 name: "primary-foreground",
153 color: tokens::PRIMARY_FOREGROUND,
154 },
155 TokenDef {
156 name: "secondary",
157 color: tokens::SECONDARY,
158 },
159 TokenDef {
160 name: "secondary-foreground",
161 color: tokens::SECONDARY_FOREGROUND,
162 },
163 TokenDef {
164 name: "muted",
165 color: tokens::MUTED,
166 },
167 TokenDef {
168 name: "muted-foreground",
169 color: tokens::MUTED_FOREGROUND,
170 },
171 TokenDef {
172 name: "accent",
173 color: tokens::ACCENT,
174 },
175 TokenDef {
176 name: "accent-foreground",
177 color: tokens::ACCENT_FOREGROUND,
178 },
179 TokenDef {
180 name: "destructive",
181 color: tokens::DESTRUCTIVE,
182 },
183 TokenDef {
184 name: "destructive-foreground",
185 color: tokens::DESTRUCTIVE_FOREGROUND,
186 },
187 TokenDef {
188 name: "border",
189 color: tokens::BORDER,
190 },
191 TokenDef {
192 name: "input",
193 color: tokens::INPUT,
194 },
195 TokenDef {
196 name: "ring",
197 color: tokens::RING,
198 },
199];
200
201const EXTENSION_TOKENS: [TokenDef; 12] = [
202 TokenDef {
203 name: "success",
204 color: tokens::SUCCESS,
205 },
206 TokenDef {
207 name: "success-foreground",
208 color: tokens::SUCCESS_FOREGROUND,
209 },
210 TokenDef {
211 name: "warning",
212 color: tokens::WARNING,
213 },
214 TokenDef {
215 name: "warning-foreground",
216 color: tokens::WARNING_FOREGROUND,
217 },
218 TokenDef {
219 name: "info",
220 color: tokens::INFO,
221 },
222 TokenDef {
223 name: "info-foreground",
224 color: tokens::INFO_FOREGROUND,
225 },
226 TokenDef {
227 name: "link-foreground",
228 color: tokens::LINK_FOREGROUND,
229 },
230 TokenDef {
231 name: "overlay-scrim",
232 color: tokens::OVERLAY_SCRIM,
233 },
234 TokenDef {
235 name: "scrollbar-thumb",
236 color: tokens::SCROLLBAR_THUMB_FILL,
237 },
238 TokenDef {
239 name: "scrollbar-thumb-active",
240 color: tokens::SCROLLBAR_THUMB_FILL_ACTIVE,
241 },
242 TokenDef {
243 name: "selection-bg",
244 color: tokens::SELECTION_BG,
245 },
246 TokenDef {
247 name: "selection-bg-unfocused",
248 color: tokens::SELECTION_BG_UNFOCUSED,
249 },
250];
251
252fn token_section(
253 title: &'static str,
254 description: &'static str,
255 tokens: &'static [TokenDef],
256 palette: &Palette,
257) -> El {
258 card([
259 card_header([card_title(title), card_description(description)]),
260 card_content([swatch_grid(tokens, palette)]),
261 ])
262 .height(Size::Fill(1.0))
263}
264
265fn swatch_grid(defs: &'static [TokenDef], palette: &Palette) -> El {
266 let rows = defs
267 .chunks(2)
268 .map(|chunk| {
269 row(chunk.iter().map(|token| token_chip(*token, palette)))
270 .gap(tokens::SPACE_3)
271 .align(Align::Center)
272 .width(Size::Fill(1.0))
273 })
274 .collect::<Vec<_>>();
275
276 column(rows)
277 .gap(tokens::SPACE_3)
278 .width(Size::Fill(1.0))
279 .height(Size::Hug)
280}
281
282fn token_chip(token: TokenDef, palette: &Palette) -> El {
283 let resolved = palette.resolve(token.color);
284 row([
285 El::new(Kind::Custom("palette-swatch"))
286 .at(file!(), line!())
287 .fill(token.color)
288 .stroke(tokens::BORDER)
289 .radius(tokens::RADIUS_SM)
290 .width(Size::Fixed(42.0))
291 .height(Size::Fixed(34.0)),
292 column([
293 text(token.name)
294 .label()
295 .ellipsis()
296 .nowrap_text()
297 .width(Size::Fill(1.0)),
298 mono(rgba_label(resolved)).caption().muted(),
299 ])
300 .gap(0.0)
301 .width(Size::Fill(1.0))
302 .height(Size::Hug),
303 ])
304 .gap(tokens::SPACE_2)
305 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_2))
306 .align(Align::Center)
307 .fill(tokens::CARD)
308 .stroke(tokens::BORDER)
309 .radius(tokens::RADIUS_MD)
310 .width(Size::Fill(1.0))
311 .height(Size::Fixed(54.0))
312}
313
314fn component_section() -> El {
315 card([
316 card_header([
317 card_title("Stock widgets"),
318 card_description("The same palette applied to regular component constructors."),
319 ]),
320 card_content([
321 row([
322 button("Primary").primary(),
323 button("Secondary").secondary(),
324 button("Outline").outline(),
325 button("Ghost").ghost(),
326 ])
327 .gap(tokens::SPACE_2)
328 .align(Align::Center),
329 row([
330 badge("success").success(),
331 badge("warning").warning(),
332 badge("destructive").destructive(),
333 badge("info").info(),
334 badge("muted").muted(),
335 ])
336 .gap(tokens::SPACE_2)
337 .align(Align::Center),
338 row([
339 text_input("palette search", &Selection::default(), "palette:search")
340 .width(Size::Fill(1.0)),
341 button_with_icon("settings", "Tune").secondary(),
342 ])
343 .gap(tokens::SPACE_2)
344 .align(Align::Center),
345 row([
346 surface_sample("Card", tokens::CARD),
347 surface_sample("Muted", tokens::MUTED),
348 surface_sample("Popover", tokens::POPOVER),
349 ])
350 .gap(tokens::SPACE_3)
351 .align(Align::Stretch),
352 ])
353 .gap(tokens::SPACE_4),
354 ])
355 .height(Size::Hug)
356}
357
358fn surface_sample(title: &'static str, fill: Color) -> El {
359 column([
360 text(title).label(),
361 text("surface sample").caption().muted(),
362 ])
363 .gap(tokens::SPACE_1)
364 .padding(tokens::SPACE_3)
365 .fill(fill)
366 .stroke(tokens::BORDER)
367 .radius(tokens::RADIUS_MD)
368 .width(Size::Fill(1.0))
369 .height(Size::Fixed(76.0))
370}Sourcepub fn dim_fill(self, c: Color) -> Self
pub fn dim_fill(self, c: Color) -> Self
Fill applied when the nearest focusable ancestor isn’t focused;
the painter lerps from dim_fill toward fill as the focus
envelope rises from 0 to 1. See Self::dim_fill field doc.
Sourcepub fn stroke(self, c: Color) -> Self
pub fn stroke(self, c: Color) -> Self
Examples found in repository?
44fn sidebar() -> El {
45 column([
46 column([h2("Aetna"), text("calibration").muted()])
47 .key("metric:sidebar.brand")
48 .gap(tokens::SPACE_1)
49 .height(Size::Hug),
50 spacer().height(Size::Fixed(tokens::SPACE_4)),
51 nav_item("01", "Overview", true),
52 nav_item("02", "Commands", false),
53 nav_item("03", "Tables", false),
54 nav_item("04", "Forms", false),
55 spacer(),
56 badge("dark theme").muted(),
57 ])
58 .gap(tokens::SPACE_2)
59 .padding(tokens::SPACE_5)
60 .key("metric:sidebar")
61 .width(Size::Fixed(220.0))
62 .height(Size::Fill(1.0))
63 .fill(tokens::CARD)
64 .stroke(tokens::BORDER)
65}
66
67fn nav_item(icon: &'static str, label: &'static str, selected: bool) -> El {
68 let mut item = row([
69 icon_cell(icon),
70 text(label)
71 .font_weight(FontWeight::Medium)
72 .ellipsis()
73 .width(Size::Fill(1.0)),
74 ])
75 .key(if selected {
76 "metric:sidebar.nav.row".to_string()
77 } else {
78 format!("nav-{label}")
79 })
80 .metrics_role(MetricsRole::ListItem)
81 .gap(tokens::SPACE_3)
82 .padding(Sides::xy(tokens::SPACE_2, 0.0))
83 .height(Size::Fixed(40.0))
84 .align(Align::Center)
85 .focusable();
86
87 if selected {
88 item = item.current();
89 }
90
91 item
92}
93
94fn main_panel() -> El {
95 column([
96 toolbar(),
97 column([
98 row([
99 kpi_card("Latency", "42 ms", "-18%", true),
100 kpi_card("Runs", "1,284", "+12%", true),
101 kpi_card("Errors", "7", "+2", false),
102 ])
103 .gap(tokens::SPACE_4),
104 row([table_card(), command_card()])
105 .gap(tokens::SPACE_4)
106 .height(Size::Fill(1.0))
107 .align(Align::Stretch),
108 ])
109 .gap(tokens::SPACE_4)
110 .height(Size::Fill(1.0))
111 .align(Align::Stretch),
112 ])
113 .padding(tokens::SPACE_7)
114 .gap(tokens::SPACE_2)
115 .width(Size::Fill(1.0))
116 .height(Size::Fill(1.0))
117}
118
119fn toolbar() -> El {
120 row([
121 column([
122 h1("Polish calibration").key("metric:page.title"),
123 text("A representative app surface for default tuning.")
124 .muted()
125 .key("metric:page.subtitle"),
126 ])
127 .gap(tokens::SPACE_2)
128 .height(Size::Hug),
129 spacer(),
130 button_with_icon("search", "Preview")
131 .secondary()
132 .key("metric:action.secondary"),
133 button_with_icon("upload", "Publish")
134 .primary()
135 .key("metric:action.primary"),
136 ])
137 .key("metric:header")
138 .gap(tokens::SPACE_4)
139 .height(Size::Hug)
140 .align(Align::Start)
141}
142
143fn kpi_card(label: &'static str, value: &'static str, delta: &'static str, positive: bool) -> El {
144 let delta_badge = if positive {
145 badge(delta).success()
146 } else {
147 badge(delta).destructive()
148 };
149 let delta_badge = if label == "Latency" {
150 delta_badge.key("metric:kpi.badge")
151 } else {
152 delta_badge
153 };
154 let value_text = h2(value).display();
155 let value_text = if label == "Latency" {
156 value_text.key("metric:kpi.value")
157 } else {
158 value_text
159 };
160 card([
161 card_header([card_title(label)]),
162 card_content([
163 row([value_text, spacer(), delta_badge]).align(Align::Center),
164 text(if positive {
165 "Moving in the expected direction"
166 } else {
167 "Needs visual attention"
168 })
169 .muted(),
170 ])
171 .gap(tokens::SPACE_6),
172 ])
173 .key(if label == "Latency" {
174 "metric:kpi.card"
175 } else {
176 label
177 })
178 .width(Size::Fill(1.0))
179}
180
181fn table_card() -> El {
182 card([
183 card_header([card_title("Reference rows")]),
184 card_content([table([
185 table_header([table_row([
186 table_head("Status").width(Size::Fixed(86.0)),
187 table_head("Surface").width(Size::Fill(1.0)),
188 table_head("Owner").width(Size::Fixed(110.0)),
189 table_head("State").width(Size::Fixed(86.0)),
190 ])
191 .key("metric:table.header")]),
192 divider(),
193 table_body([
194 data_row("OK", "Settings card", "core", "selected", true, "success"),
195 data_row(
196 "WARN",
197 "Command palette density",
198 "widgets",
199 "needs work",
200 false,
201 "warning",
202 ),
203 data_row(
204 "ERR",
205 "Disabled and invalid states",
206 "style",
207 "missing",
208 false,
209 "destructive",
210 ),
211 data_row(
212 "INFO",
213 "Token resolution",
214 "theme",
215 "planned",
216 false,
217 "info",
218 ),
219 data_row(
220 "OK",
221 "Popover elevation",
222 "shader",
223 "queued",
224 false,
225 "success",
226 ),
227 ])
228 .gap(tokens::SPACE_1)
229 .width(Size::Fill(1.0)),
230 ])]),
231 ])
232 .key("metric:table.card")
233 .width(Size::Fill(1.2))
234 .height(Size::Fill(1.0))
235}
236
237fn data_row(
238 status: &'static str,
239 title: &'static str,
240 owner: &'static str,
241 state: &'static str,
242 selected: bool,
243 tone: &'static str,
244) -> El {
245 let status_badge = match tone {
246 "success" => badge(status).success(),
247 "warning" => badge(status).warning(),
248 "destructive" => badge(status).destructive(),
249 _ => badge(status).info(),
250 };
251 let status_badge = if selected {
252 status_badge.key("metric:table.badge")
253 } else {
254 status_badge
255 };
256
257 let mut row = table_row([
258 table_cell(status_badge).width(Size::Fixed(70.0)),
259 column([
260 text(title)
261 .font_weight(FontWeight::Medium)
262 .ellipsis()
263 .width(Size::Fill(1.0)),
264 text("Default styling probe.")
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0)),
271 table_cell(text(owner).muted()).width(Size::Fixed(110.0)),
272 table_cell(text(state).label().small()).width(Size::Fixed(86.0)),
273 ])
274 .key(if selected {
275 "metric:table.row".to_string()
276 } else {
277 format!("row-{title}")
278 })
279 .focusable();
280
281 if selected {
282 row = row.selected();
283 }
284
285 row
286}
287
288fn command_card() -> El {
289 card([
290 card_header([card_title("Command surface")]),
291 card_content([
292 text_input(
293 "Search commands...",
294 &Selection::default(),
295 "command-search",
296 )
297 .key("metric:command.input")
298 .width(Size::Fill(1.0)),
299 popover_panel([
300 command_row("git-branch", "New branch", "Ctrl+B").key("metric:command.row"),
301 command_row("git-commit", "Commit staged files", "Ctrl+Enter")
302 .key("command-row-commit"),
303 command_row("refresh-cw", "Refresh repository", "Ctrl+R")
304 .key("command-row-refresh"),
305 command_row("alert-circle", "Force push", "Danger").key("command-row-force"),
306 ])
307 .width(Size::Fill(1.0)),
308 scroll([form_probe()]).key("form-probe-scroll"),
309 ])
310 .height(Size::Fill(1.0)),
311 ])
312 .key("metric:command.card")
313 .width(Size::Fill(0.8))
314 .height(Size::Fill(1.0))
315}
316
317fn form_probe() -> El {
318 form([
319 form_item([
320 form_label("Valid input"),
321 form_control(
322 text_input(
323 "Valid input",
324 &Selection::caret("valid-input", 11),
325 "valid-input",
326 )
327 .key("metric:form.input"),
328 ),
329 form_description("Default field spacing and helper text."),
330 ]),
331 form_item([
332 form_label("Invalid input"),
333 form_control(
334 text_input(
335 "Invalid input",
336 &Selection::caret("invalid-input", 13),
337 "invalid-input",
338 )
339 .invalid(),
340 ),
341 form_message("This field needs attention."),
342 ]),
343 row([
344 button("Disabled").secondary().disabled(),
345 button("Loading").primary().loading(),
346 spacer(),
347 ]),
348 ])
349 .padding(tokens::SPACE_3)
350 .fill(tokens::MUTED)
351 .stroke(tokens::BORDER)
352 .radius(tokens::RADIUS_MD)
353}
354
355fn icon_cell(label: &'static str) -> El {
356 El::new(Kind::Custom("icon_cell"))
357 .style_profile(StyleProfile::Surface)
358 .text(label)
359 .text_align(TextAlign::Center)
360 .caption()
361 .font_weight(FontWeight::Semibold)
362 .fill(tokens::MUTED)
363 .stroke(tokens::BORDER)
364 .radius(tokens::RADIUS_SM)
365 .width(Size::Fixed(26.0))
366 .height(Size::Fixed(26.0))
367}More examples
282fn token_chip(token: TokenDef, palette: &Palette) -> El {
283 let resolved = palette.resolve(token.color);
284 row([
285 El::new(Kind::Custom("palette-swatch"))
286 .at(file!(), line!())
287 .fill(token.color)
288 .stroke(tokens::BORDER)
289 .radius(tokens::RADIUS_SM)
290 .width(Size::Fixed(42.0))
291 .height(Size::Fixed(34.0)),
292 column([
293 text(token.name)
294 .label()
295 .ellipsis()
296 .nowrap_text()
297 .width(Size::Fill(1.0)),
298 mono(rgba_label(resolved)).caption().muted(),
299 ])
300 .gap(0.0)
301 .width(Size::Fill(1.0))
302 .height(Size::Hug),
303 ])
304 .gap(tokens::SPACE_2)
305 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_2))
306 .align(Align::Center)
307 .fill(tokens::CARD)
308 .stroke(tokens::BORDER)
309 .radius(tokens::RADIUS_MD)
310 .width(Size::Fill(1.0))
311 .height(Size::Fixed(54.0))
312}
313
314fn component_section() -> El {
315 card([
316 card_header([
317 card_title("Stock widgets"),
318 card_description("The same palette applied to regular component constructors."),
319 ]),
320 card_content([
321 row([
322 button("Primary").primary(),
323 button("Secondary").secondary(),
324 button("Outline").outline(),
325 button("Ghost").ghost(),
326 ])
327 .gap(tokens::SPACE_2)
328 .align(Align::Center),
329 row([
330 badge("success").success(),
331 badge("warning").warning(),
332 badge("destructive").destructive(),
333 badge("info").info(),
334 badge("muted").muted(),
335 ])
336 .gap(tokens::SPACE_2)
337 .align(Align::Center),
338 row([
339 text_input("palette search", &Selection::default(), "palette:search")
340 .width(Size::Fill(1.0)),
341 button_with_icon("settings", "Tune").secondary(),
342 ])
343 .gap(tokens::SPACE_2)
344 .align(Align::Center),
345 row([
346 surface_sample("Card", tokens::CARD),
347 surface_sample("Muted", tokens::MUTED),
348 surface_sample("Popover", tokens::POPOVER),
349 ])
350 .gap(tokens::SPACE_3)
351 .align(Align::Stretch),
352 ])
353 .gap(tokens::SPACE_4),
354 ])
355 .height(Size::Hug)
356}
357
358fn surface_sample(title: &'static str, fill: Color) -> El {
359 column([
360 text(title).label(),
361 text("surface sample").caption().muted(),
362 ])
363 .gap(tokens::SPACE_1)
364 .padding(tokens::SPACE_3)
365 .fill(fill)
366 .stroke(tokens::BORDER)
367 .radius(tokens::RADIUS_MD)
368 .width(Size::Fill(1.0))
369 .height(Size::Fixed(76.0))
370}41fn settings_sidebar() -> El {
42 column([
43 row([
44 icon_slot("settings"),
45 column([
46 text("Workspace")
47 .semibold()
48 .ellipsis()
49 .width(Size::Fill(1.0)),
50 text("Settings").caption().ellipsis().width(Size::Fill(1.0)),
51 ])
52 .gap(2.0)
53 .width(Size::Fill(1.0))
54 .height(Size::Hug),
55 ])
56 .gap(tokens::SPACE_2)
57 .height(Size::Fixed(44.0))
58 .align(Align::Center),
59 section_label("Personal"),
60 side_item("users", "Profile", false),
61 side_item("settings", "Account", true),
62 side_item("alert-circle", "Security", false),
63 side_item("bell", "Notifications", false),
64 spacer().height(Size::Fixed(tokens::SPACE_4)),
65 section_label("Workspace"),
66 side_item("file-text", "Billing", false),
67 side_item("bar-chart", "Appearance", false),
68 side_item("activity", "Integrations", false),
69 spacer(),
70 column([text("Changes sync after save.").caption().wrap_text()])
71 .padding(tokens::SPACE_2)
72 .fill(tokens::MUTED)
73 .radius(tokens::RADIUS_MD),
74 ])
75 .gap(tokens::SPACE_2)
76 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
77 .key("metric:sidebar")
78 .width(Size::Fixed(244.0))
79 .height(Size::Fill(1.0))
80 .fill(tokens::CARD)
81 .stroke(tokens::BORDER)
82}
83
84fn settings_main() -> El {
85 column([
86 settings_header(),
87 row([settings_nav_card(), settings_body(), settings_aside()])
88 .gap(tokens::SPACE_4)
89 .padding(tokens::SPACE_4)
90 .height(Size::Fill(1.0))
91 .align(Align::Stretch),
92 ])
93 .width(Size::Fill(1.0))
94 .height(Size::Fill(1.0))
95}
96
97fn settings_header() -> El {
98 row([
99 icon_button("menu").ghost(),
100 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
101 h3("Settings").key("metric:page.title"),
102 spacer(),
103 button("Reset").secondary(),
104 button("Save changes").primary(),
105 ])
106 .key("metric:header")
107 .gap(tokens::SPACE_3)
108 .height(Size::Fixed(56.0))
109 .padding(Sides::xy(tokens::SPACE_4, 0.0))
110 .align(Align::Center)
111 .stroke(tokens::BORDER)
112}
113
114fn settings_nav_card() -> El {
115 column([
116 settings_nav_item("Account", true),
117 settings_nav_item("Security", false),
118 settings_nav_item("Notifications", false),
119 settings_nav_item("Appearance", false),
120 settings_nav_item("Billing", false),
121 ])
122 .gap(tokens::SPACE_1)
123 .padding(tokens::SPACE_1)
124 .width(Size::Fixed(220.0))
125 .height(Size::Fill(1.0))
126 .style_profile(StyleProfile::Surface)
127 .surface_role(SurfaceRole::Panel)
128 .fill(tokens::CARD)
129 .stroke(tokens::BORDER)
130 .radius(tokens::RADIUS_MD)
131 .shadow(tokens::SHADOW_MD)
132}
133
134fn settings_nav_item(label: &'static str, selected: bool) -> El {
135 let mut item = row([
136 El::new(Kind::Custom("nav-dot"))
137 .fill(tokens::MUTED_FOREGROUND)
138 .radius(tokens::RADIUS_PILL)
139 .width(Size::Fixed(6.0))
140 .height(Size::Fixed(6.0)),
141 text(label)
142 .font_weight(FontWeight::Medium)
143 .ellipsis()
144 .width(Size::Fill(1.0)),
145 ])
146 .key(if selected {
147 "metric:settings.nav.row".to_string()
148 } else {
149 format!("settings-nav-{label}")
150 })
151 .metrics_role(MetricsRole::ListItem)
152 .align(Align::Center)
153 .focusable();
154
155 if selected {
156 item = item.current();
157 } else {
158 item = item.color(tokens::MUTED_FOREGROUND);
159 }
160
161 item
162}
163
164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}
185
186fn profile_card() -> El {
187 card([
188 card_header([
189 card_title("Profile"),
190 card_description("This information appears in audit logs and shared documents."),
191 ]),
192 card_content([form([
193 row([
194 setting_field("Display name", "Alicia Koch", "display-name"),
195 setting_field("Email", "alicia@acme.co", "email"),
196 ])
197 .gap(tokens::SPACE_3),
198 row([
199 setting_select("Role", "Workspace admin", "role"),
200 setting_select("Region", "US East", "region"),
201 ])
202 .gap(tokens::SPACE_3),
203 ])]),
204 ])
205 .key("metric:profile.card")
206}
207
208fn setting_field(label: &'static str, value: &'static str, key: &'static str) -> El {
209 form_item([
210 form_label(label),
211 form_control(
212 text_input(value, &Selection::caret(key, value.len()), key).key(
213 if key == "display-name" {
214 "metric:form.input"
215 } else {
216 key
217 },
218 ),
219 ),
220 ])
221 .width(Size::Fill(1.0))
222}
223
224fn setting_select(label: &'static str, value: &'static str, key: &'static str) -> El {
225 form_item([form_label(label), form_control(select_trigger(key, value))]).width(Size::Fill(1.0))
226}
227
228fn preferences_card() -> El {
229 card([
230 card_header([
231 card_title("Preferences"),
232 card_description("Defaults used when creating new dashboards and exports."),
233 ]),
234 card_content([column([
235 preference_row(
236 "Compact navigation",
237 "Use tighter rows in the sidebar and command menus.",
238 switch(true).key("compact-navigation"),
239 ),
240 divider(),
241 preference_row(
242 "Email summaries",
243 "Send a daily digest when documents change.",
244 switch(false).key("email-summaries"),
245 ),
246 divider(),
247 preference_row(
248 "Require approval",
249 "Route external sharing through an owner review.",
250 checkbox(true).key("approval-required"),
251 ),
252 ])
253 .gap(0.0)
254 .width(Size::Fill(1.0))])
255 .padding(0.0),
256 ])
257 .key("metric:preferences.card")
258}
259
260fn preference_row(title: &'static str, description: &'static str, control: El) -> El {
261 row([
262 column([
263 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
264 text(description)
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0))
271 .height(Size::Hug),
272 control,
273 ])
274 .key(if title == "Compact navigation" {
275 "metric:preference.row".to_string()
276 } else {
277 format!("preference-{title}")
278 })
279 .metrics_role(MetricsRole::PreferenceRow)
280 .gap(tokens::SPACE_4)
281 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_3))
282 .align(Align::Center)
283}
284
285fn settings_aside() -> El {
286 column([security_card(), scale_card()])
287 .gap(tokens::SPACE_4)
288 .width(Size::Fixed(300.0))
289 .height(Size::Fill(1.0))
290}
291
292fn security_card() -> El {
293 card([
294 card_header([
295 card_title("Security"),
296 card_description("Two-factor authentication is enabled for all privileged users."),
297 ]),
298 card_content([
299 compact_stat("Passkeys", "2 registered", badge("On").success()),
300 compact_stat("Sessions", "3 active", button("Review").secondary()),
301 ]),
302 ])
303 .width(Size::Fill(1.0))
304}
305
306fn scale_card() -> El {
307 card([
308 card_header([
309 card_title("Interface scale"),
310 card_description("Reference captures keep browser zoom fixed and vary root UI scale."),
311 ]),
312 card_content([
313 row([text("Dense").caption(), spacer(), text("Default").caption()]),
314 slider(0.66, tokens::PRIMARY)
315 .key("interface-scale")
316 .width(Size::Fill(1.0)),
317 ]),
318 ])
319 .width(Size::Fill(1.0))
320}
321
322fn compact_stat(title: &'static str, detail: &'static str, control: El) -> El {
323 row([
324 column([
325 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
326 text(detail).caption().ellipsis().width(Size::Fill(1.0)),
327 ])
328 .gap(2.0)
329 .width(Size::Fill(1.0))
330 .height(Size::Hug),
331 control,
332 ])
333 .gap(tokens::SPACE_2)
334 .height(Size::Fixed(44.0))
335 .align(Align::Center)
336}
337
338fn section_label(label: &'static str) -> El {
339 text(label)
340 .caption()
341 .height(Size::Fixed(22.0))
342 .padding(Sides::xy(tokens::SPACE_2, 0.0))
343}
344
345fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
346 let mut item = row([
347 icon(icon_name)
348 .color(tokens::MUTED_FOREGROUND)
349 .icon_size(tokens::ICON_SM)
350 .width(Size::Fixed(tokens::ICON_SM)),
351 text(label)
352 .font_weight(FontWeight::Medium)
353 .ellipsis()
354 .width(Size::Fill(1.0)),
355 ])
356 .key(if selected {
357 "metric:sidebar.nav.row".to_string()
358 } else {
359 format!("side-item-{label}")
360 })
361 .metrics_role(MetricsRole::ListItem)
362 .gap(tokens::SPACE_2)
363 .padding(Sides::xy(tokens::SPACE_2, 0.0))
364 .height(Size::Fixed(32.0))
365 .align(Align::Center)
366 .focusable();
367
368 if selected {
369 item = item.current();
370 } else {
371 item = item.color(tokens::MUTED_FOREGROUND);
372 }
373
374 item
375}
376
377fn icon_slot(icon_name: &'static str) -> El {
378 El::new(Kind::Custom("icon_cell"))
379 .style_profile(StyleProfile::Surface)
380 .child(
381 icon(icon_name)
382 .color(tokens::FOREGROUND)
383 .icon_size(tokens::ICON_XS),
384 )
385 .align(Align::Center)
386 .justify(Justify::Center)
387 .fill(tokens::MUTED)
388 .stroke(tokens::BORDER)
389 .radius(tokens::RADIUS_SM)
390 .width(Size::Fixed(30.0))
391 .height(Size::Fixed(30.0))
392}41fn dashboard_sidebar() -> El {
42 column([
43 row([
44 icon_cell("A"),
45 column([
46 text("Acme Inc.")
47 .semibold()
48 .ellipsis()
49 .width(Size::Fill(1.0)),
50 text("Enterprise")
51 .caption()
52 .ellipsis()
53 .width(Size::Fill(1.0)),
54 ])
55 .gap(2.0)
56 .width(Size::Fill(1.0))
57 .height(Size::Hug),
58 ])
59 .gap(tokens::SPACE_2)
60 .height(Size::Fixed(44.0))
61 .align(Align::Center),
62 section_label("Platform"),
63 side_item("layout-dashboard", "Dashboard", true),
64 side_item("activity", "Lifecycle", false),
65 side_item("bar-chart", "Analytics", false),
66 side_item("folder", "Projects", false),
67 spacer().height(Size::Fixed(tokens::SPACE_4)),
68 section_label("Documents"),
69 side_item("file-text", "Data library", false),
70 side_item("download", "Reports", false),
71 side_item("users", "Team", false),
72 spacer(),
73 row([
74 icon_cell("AK"),
75 column([
76 text("Alicia Koch")
77 .semibold()
78 .ellipsis()
79 .width(Size::Fill(1.0)),
80 text("alicia@example.com")
81 .caption()
82 .ellipsis()
83 .width(Size::Fill(1.0)),
84 ])
85 .gap(2.0)
86 .width(Size::Fill(1.0))
87 .height(Size::Hug),
88 ])
89 .gap(tokens::SPACE_2)
90 .height(Size::Fixed(50.0))
91 .align(Align::Center),
92 ])
93 .gap(tokens::SPACE_2)
94 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
95 .key("metric:sidebar")
96 .width(Size::Fixed(244.0))
97 .height(Size::Fill(1.0))
98 .fill(tokens::CARD)
99 .stroke(tokens::BORDER)
100}
101
102fn section_label(label: &'static str) -> El {
103 text(label)
104 .caption()
105 .height(Size::Fixed(22.0))
106 .padding(Sides::xy(tokens::SPACE_2, 0.0))
107}
108
109fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
110 let mut item = row([
111 icon(icon_name)
112 .color(tokens::MUTED_FOREGROUND)
113 .icon_size(tokens::ICON_SM)
114 .width(Size::Fixed(tokens::ICON_SM)),
115 text(label)
116 .font_weight(FontWeight::Medium)
117 .ellipsis()
118 .width(Size::Fill(1.0)),
119 ])
120 .key(if selected {
121 "metric:sidebar.nav.row".to_string()
122 } else {
123 format!("side-item-{label}")
124 })
125 .metrics_role(MetricsRole::ListItem)
126 .gap(tokens::SPACE_2)
127 .padding(Sides::xy(tokens::SPACE_2, 0.0))
128 .height(Size::Fixed(32.0))
129 .align(Align::Center)
130 .focusable();
131
132 if selected {
133 item = item.current();
134 } else {
135 item = item.color(tokens::MUTED_FOREGROUND);
136 }
137
138 item
139}
140
141fn dashboard_main() -> El {
142 column([
143 dashboard_header(),
144 column([
145 row([
146 metric_card(
147 "bar-chart",
148 "Total Revenue",
149 "$1,250.00",
150 "+12.5%",
151 "Trending up this month",
152 true,
153 ),
154 metric_card(
155 "users",
156 "New Customers",
157 "1,234",
158 "-20%",
159 "Acquisition needs attention",
160 false,
161 ),
162 metric_card(
163 "folder",
164 "Active Accounts",
165 "45,678",
166 "+12.5%",
167 "Strong user retention",
168 true,
169 ),
170 metric_card(
171 "activity",
172 "Growth Rate",
173 "4.5%",
174 "+4.5%",
175 "Meets growth projections",
176 true,
177 ),
178 ])
179 .gap(tokens::SPACE_4),
180 row([chart_card(), sales_card()])
181 .gap(tokens::SPACE_4)
182 .height(Size::Fixed(306.0))
183 .align(Align::Stretch),
184 documents_card(),
185 ])
186 .gap(tokens::SPACE_4)
187 .padding(tokens::SPACE_7)
188 .height(Size::Fill(1.0)),
189 ])
190 .width(Size::Fill(1.0))
191 .height(Size::Fill(1.0))
192}
193
194fn dashboard_header() -> El {
195 row([
196 icon_button("menu").ghost(),
197 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
198 h3("Documents").key("metric:page.title"),
199 spacer(),
200 text_input("Search...", &Selection::default(), "dashboard-search")
201 .key("metric:command.input")
202 .width(Size::Fixed(260.0)),
203 icon_button("plus").ghost(),
204 icon_button("bell").ghost(),
205 ])
206 .key("metric:header")
207 .gap(tokens::SPACE_3)
208 .height(Size::Fixed(56.0))
209 .padding(Sides::xy(tokens::SPACE_4, 0.0))
210 .align(Align::Center)
211 .stroke(tokens::BORDER)
212}35fn fixture() -> El {
36 column([
37 h2("Scrollbar"),
38 text("scroll() and virtual_list() show a draggable thumb by default.").muted(),
39 row([
40 // 1) scroll() — default-on scrollbar.
41 column([
42 text("scroll() — default").bold(),
43 scroll(list_rows())
44 .height(Size::Fixed(240.0))
45 .padding(tokens::SPACE_2)
46 .stroke(tokens::BORDER)
47 .stroke_width(1.0)
48 .radius(tokens::RADIUS_MD),
49 ])
50 .gap(tokens::SPACE_2)
51 .width(Size::Fill(1.0))
52 .height(Size::Hug),
53 // 2) virtual_list — thumb scales to content size.
54 column([
55 text("virtual_list(200, 28)").bold(),
56 virtual_list(200, 28.0, |i| {
57 row([
58 text(format!("{i:03}")).mono().muted(),
59 text(format!("row {i}")),
60 ])
61 .gap(tokens::SPACE_2)
62 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_1))
63 .height(Size::Fixed(28.0))
64 .align(Align::Center)
65 })
66 .height(Size::Fixed(240.0))
67 .padding(tokens::SPACE_2)
68 .stroke(tokens::BORDER)
69 .stroke_width(1.0)
70 .radius(tokens::RADIUS_MD),
71 ])
72 .gap(tokens::SPACE_2)
73 .width(Size::Fill(1.0))
74 .height(Size::Hug),
75 // 3) Opt-out: same content, no thumb.
76 column([
77 text("scroll().no_scrollbar()").bold(),
78 scroll(list_rows())
79 .no_scrollbar()
80 .height(Size::Fixed(240.0))
81 .padding(tokens::SPACE_2)
82 .stroke(tokens::BORDER)
83 .stroke_width(1.0)
84 .radius(tokens::RADIUS_MD),
85 ])
86 .gap(tokens::SPACE_2)
87 .width(Size::Fill(1.0))
88 .height(Size::Hug),
89 ])
90 .gap(tokens::SPACE_4)
91 .width(Size::Fill(1.0)),
92 ])
93 .gap(tokens::SPACE_4)
94 .padding(tokens::SPACE_7)
95}Sourcepub fn stroke_width(self, w: f32) -> Self
pub fn stroke_width(self, w: f32) -> Self
Examples found in repository?
35fn fixture() -> El {
36 column([
37 h2("Scrollbar"),
38 text("scroll() and virtual_list() show a draggable thumb by default.").muted(),
39 row([
40 // 1) scroll() — default-on scrollbar.
41 column([
42 text("scroll() — default").bold(),
43 scroll(list_rows())
44 .height(Size::Fixed(240.0))
45 .padding(tokens::SPACE_2)
46 .stroke(tokens::BORDER)
47 .stroke_width(1.0)
48 .radius(tokens::RADIUS_MD),
49 ])
50 .gap(tokens::SPACE_2)
51 .width(Size::Fill(1.0))
52 .height(Size::Hug),
53 // 2) virtual_list — thumb scales to content size.
54 column([
55 text("virtual_list(200, 28)").bold(),
56 virtual_list(200, 28.0, |i| {
57 row([
58 text(format!("{i:03}")).mono().muted(),
59 text(format!("row {i}")),
60 ])
61 .gap(tokens::SPACE_2)
62 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_1))
63 .height(Size::Fixed(28.0))
64 .align(Align::Center)
65 })
66 .height(Size::Fixed(240.0))
67 .padding(tokens::SPACE_2)
68 .stroke(tokens::BORDER)
69 .stroke_width(1.0)
70 .radius(tokens::RADIUS_MD),
71 ])
72 .gap(tokens::SPACE_2)
73 .width(Size::Fill(1.0))
74 .height(Size::Hug),
75 // 3) Opt-out: same content, no thumb.
76 column([
77 text("scroll().no_scrollbar()").bold(),
78 scroll(list_rows())
79 .no_scrollbar()
80 .height(Size::Fixed(240.0))
81 .padding(tokens::SPACE_2)
82 .stroke(tokens::BORDER)
83 .stroke_width(1.0)
84 .radius(tokens::RADIUS_MD),
85 ])
86 .gap(tokens::SPACE_2)
87 .width(Size::Fill(1.0))
88 .height(Size::Hug),
89 ])
90 .gap(tokens::SPACE_4)
91 .width(Size::Fill(1.0)),
92 ])
93 .gap(tokens::SPACE_4)
94 .padding(tokens::SPACE_7)
95}Sourcepub fn radius(self, r: impl Into<Corners>) -> Self
pub fn radius(self, r: impl Into<Corners>) -> Self
Set the element’s corner radii. A scalar (e.g.
.radius(tokens::RADIUS_MD)) sets all four corners uniformly
via Corners::from; pass Corners::top / Corners::bottom
/ Corners::left / Corners::right, or a directly-built
Corners, to round only a subset of corners.
Examples found in repository?
303fn bar(height: f32, color: Color) -> El {
304 El::new(Kind::Custom("chart_bar"))
305 .fill(color)
306 .radius(tokens::RADIUS_SM)
307 .width(Size::Fill(1.0))
308 .height(Size::Fixed(height))
309}
310
311fn sales_card() -> El {
312 card([
313 card_header([
314 card_title("Recent Sales"),
315 card_description("You made 265 sales this month."),
316 ])
317 .padding(tokens::SPACE_4),
318 card_content([
319 sale_row("OM", "Olivia Martin", "olivia@example.com", "+$1,999.00"),
320 sale_row("JL", "Jackson Lee", "jackson@example.com", "+$39.00"),
321 sale_row("IN", "Isabella Nguyen", "isabella@example.com", "+$299.00"),
322 sale_row("WK", "William Kim", "will@example.com", "+$99.00"),
323 ])
324 .gap(tokens::SPACE_2)
325 .padding(Sides {
326 left: tokens::SPACE_4,
327 right: tokens::SPACE_4,
328 top: 0.0,
329 bottom: tokens::SPACE_4,
330 }),
331 ])
332 .key("metric:sales.card")
333 .width(Size::Fixed(330.0))
334 .height(Size::Fill(1.0))
335}
336
337fn sale_row(
338 initials: &'static str,
339 name: &'static str,
340 email: &'static str,
341 amount: &'static str,
342) -> El {
343 row([
344 icon_cell(initials),
345 column([
346 text(name).semibold().ellipsis().width(Size::Fill(1.0)),
347 text(email).caption().ellipsis().width(Size::Fill(1.0)),
348 ])
349 .gap(2.0)
350 .height(Size::Hug)
351 .width(Size::Fill(1.0)),
352 text(amount).label().small(),
353 ])
354 .gap(tokens::SPACE_2)
355 .height(Size::Fixed(42.0))
356 .align(Align::Center)
357}
358
359fn documents_card() -> El {
360 card([
361 card_header([card_title("Documents")]).padding(tokens::SPACE_4),
362 card_content([scroll([table([
363 table_header([table_row([
364 table_head("").width(Size::Fixed(35.0)),
365 table_head("Header").width(Size::Fill(1.8)),
366 table_head("Section Type").width(Size::Fill(1.0)),
367 table_head("Status").width(Size::Fixed(104.0)),
368 table_head("Target").width(Size::Fixed(64.0)),
369 table_head("Limit").width(Size::Fixed(64.0)),
370 table_head("Reviewer").width(Size::Fixed(128.0)),
371 table_head("").width(Size::Fixed(32.0)),
372 ])
373 .padding(Sides::xy(tokens::SPACE_4, 0.0))
374 .key("metric:table.header")]),
375 divider(),
376 table_body([
377 document_row(
378 "Cover page",
379 "Cover page",
380 "In Process",
381 "18",
382 "5",
383 "Eddie Lake",
384 "info",
385 ),
386 document_row(
387 "Table of contents",
388 "Table of contents",
389 "Done",
390 "29",
391 "24",
392 "Eddie Lake",
393 "success",
394 ),
395 ]),
396 ])])
397 .height(Size::Fill(1.0))])
398 .gap(0.0)
399 .padding(0.0)
400 .height(Size::Fill(1.0)),
401 ])
402 .key("metric:table.card")
403 .height(Size::Fill(1.0))
404}
405
406fn document_row(
407 header: &'static str,
408 section: &'static str,
409 status: &'static str,
410 target: &'static str,
411 limit: &'static str,
412 reviewer: &'static str,
413 tone: &'static str,
414) -> El {
415 let status_badge = match tone {
416 "success" => badge(status).success(),
417 _ => badge(status).info(),
418 };
419 table_row([
420 table_utility_cell("::"),
421 table_cell(text(header).label().small()).width(Size::Fill(1.8)),
422 table_cell(text(section).muted()).width(Size::Fill(1.0)),
423 table_cell(status_badge).width(Size::Fixed(104.0)),
424 table_cell(text(target).label().small()).width(Size::Fixed(64.0)),
425 table_cell(text(limit).label().small()).width(Size::Fixed(64.0)),
426 table_cell(text(reviewer).muted()).width(Size::Fixed(128.0)),
427 table_action_cell(),
428 ])
429 .padding(Sides::xy(tokens::SPACE_4, 0.0))
430 .key(if header == "Cover page" {
431 "metric:table.row"
432 } else {
433 header
434 })
435}
436
437fn table_utility_cell(label: &'static str) -> El {
438 table_cell(text(label).muted().center_text()).width(Size::Fixed(35.0))
439}
440
441fn table_action_cell() -> El {
442 stack([icon("more-horizontal")
443 .icon_size(tokens::ICON_SM)
444 .color(tokens::MUTED_FOREGROUND)])
445 .align(Align::Center)
446 .justify(Justify::Center)
447 .width(Size::Fixed(32.0))
448 .height(Size::Hug)
449}
450
451fn icon_cell(label: &'static str) -> El {
452 El::new(Kind::Custom("icon_cell"))
453 .style_profile(StyleProfile::Surface)
454 .text(label)
455 .text_align(TextAlign::Center)
456 .caption()
457 .font_weight(FontWeight::Semibold)
458 .fill(tokens::MUTED)
459 .radius(tokens::RADIUS_SM)
460 .width(Size::Fixed(30.0))
461 .height(Size::Fixed(30.0))
462}More examples
282fn token_chip(token: TokenDef, palette: &Palette) -> El {
283 let resolved = palette.resolve(token.color);
284 row([
285 El::new(Kind::Custom("palette-swatch"))
286 .at(file!(), line!())
287 .fill(token.color)
288 .stroke(tokens::BORDER)
289 .radius(tokens::RADIUS_SM)
290 .width(Size::Fixed(42.0))
291 .height(Size::Fixed(34.0)),
292 column([
293 text(token.name)
294 .label()
295 .ellipsis()
296 .nowrap_text()
297 .width(Size::Fill(1.0)),
298 mono(rgba_label(resolved)).caption().muted(),
299 ])
300 .gap(0.0)
301 .width(Size::Fill(1.0))
302 .height(Size::Hug),
303 ])
304 .gap(tokens::SPACE_2)
305 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_2))
306 .align(Align::Center)
307 .fill(tokens::CARD)
308 .stroke(tokens::BORDER)
309 .radius(tokens::RADIUS_MD)
310 .width(Size::Fill(1.0))
311 .height(Size::Fixed(54.0))
312}
313
314fn component_section() -> El {
315 card([
316 card_header([
317 card_title("Stock widgets"),
318 card_description("The same palette applied to regular component constructors."),
319 ]),
320 card_content([
321 row([
322 button("Primary").primary(),
323 button("Secondary").secondary(),
324 button("Outline").outline(),
325 button("Ghost").ghost(),
326 ])
327 .gap(tokens::SPACE_2)
328 .align(Align::Center),
329 row([
330 badge("success").success(),
331 badge("warning").warning(),
332 badge("destructive").destructive(),
333 badge("info").info(),
334 badge("muted").muted(),
335 ])
336 .gap(tokens::SPACE_2)
337 .align(Align::Center),
338 row([
339 text_input("palette search", &Selection::default(), "palette:search")
340 .width(Size::Fill(1.0)),
341 button_with_icon("settings", "Tune").secondary(),
342 ])
343 .gap(tokens::SPACE_2)
344 .align(Align::Center),
345 row([
346 surface_sample("Card", tokens::CARD),
347 surface_sample("Muted", tokens::MUTED),
348 surface_sample("Popover", tokens::POPOVER),
349 ])
350 .gap(tokens::SPACE_3)
351 .align(Align::Stretch),
352 ])
353 .gap(tokens::SPACE_4),
354 ])
355 .height(Size::Hug)
356}
357
358fn surface_sample(title: &'static str, fill: Color) -> El {
359 column([
360 text(title).label(),
361 text("surface sample").caption().muted(),
362 ])
363 .gap(tokens::SPACE_1)
364 .padding(tokens::SPACE_3)
365 .fill(fill)
366 .stroke(tokens::BORDER)
367 .radius(tokens::RADIUS_MD)
368 .width(Size::Fill(1.0))
369 .height(Size::Fixed(76.0))
370}317fn form_probe() -> El {
318 form([
319 form_item([
320 form_label("Valid input"),
321 form_control(
322 text_input(
323 "Valid input",
324 &Selection::caret("valid-input", 11),
325 "valid-input",
326 )
327 .key("metric:form.input"),
328 ),
329 form_description("Default field spacing and helper text."),
330 ]),
331 form_item([
332 form_label("Invalid input"),
333 form_control(
334 text_input(
335 "Invalid input",
336 &Selection::caret("invalid-input", 13),
337 "invalid-input",
338 )
339 .invalid(),
340 ),
341 form_message("This field needs attention."),
342 ]),
343 row([
344 button("Disabled").secondary().disabled(),
345 button("Loading").primary().loading(),
346 spacer(),
347 ]),
348 ])
349 .padding(tokens::SPACE_3)
350 .fill(tokens::MUTED)
351 .stroke(tokens::BORDER)
352 .radius(tokens::RADIUS_MD)
353}
354
355fn icon_cell(label: &'static str) -> El {
356 El::new(Kind::Custom("icon_cell"))
357 .style_profile(StyleProfile::Surface)
358 .text(label)
359 .text_align(TextAlign::Center)
360 .caption()
361 .font_weight(FontWeight::Semibold)
362 .fill(tokens::MUTED)
363 .stroke(tokens::BORDER)
364 .radius(tokens::RADIUS_SM)
365 .width(Size::Fixed(26.0))
366 .height(Size::Fixed(26.0))
367}41fn settings_sidebar() -> El {
42 column([
43 row([
44 icon_slot("settings"),
45 column([
46 text("Workspace")
47 .semibold()
48 .ellipsis()
49 .width(Size::Fill(1.0)),
50 text("Settings").caption().ellipsis().width(Size::Fill(1.0)),
51 ])
52 .gap(2.0)
53 .width(Size::Fill(1.0))
54 .height(Size::Hug),
55 ])
56 .gap(tokens::SPACE_2)
57 .height(Size::Fixed(44.0))
58 .align(Align::Center),
59 section_label("Personal"),
60 side_item("users", "Profile", false),
61 side_item("settings", "Account", true),
62 side_item("alert-circle", "Security", false),
63 side_item("bell", "Notifications", false),
64 spacer().height(Size::Fixed(tokens::SPACE_4)),
65 section_label("Workspace"),
66 side_item("file-text", "Billing", false),
67 side_item("bar-chart", "Appearance", false),
68 side_item("activity", "Integrations", false),
69 spacer(),
70 column([text("Changes sync after save.").caption().wrap_text()])
71 .padding(tokens::SPACE_2)
72 .fill(tokens::MUTED)
73 .radius(tokens::RADIUS_MD),
74 ])
75 .gap(tokens::SPACE_2)
76 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_2))
77 .key("metric:sidebar")
78 .width(Size::Fixed(244.0))
79 .height(Size::Fill(1.0))
80 .fill(tokens::CARD)
81 .stroke(tokens::BORDER)
82}
83
84fn settings_main() -> El {
85 column([
86 settings_header(),
87 row([settings_nav_card(), settings_body(), settings_aside()])
88 .gap(tokens::SPACE_4)
89 .padding(tokens::SPACE_4)
90 .height(Size::Fill(1.0))
91 .align(Align::Stretch),
92 ])
93 .width(Size::Fill(1.0))
94 .height(Size::Fill(1.0))
95}
96
97fn settings_header() -> El {
98 row([
99 icon_button("menu").ghost(),
100 divider().width(Size::Fixed(1.0)).height(Size::Fixed(22.0)),
101 h3("Settings").key("metric:page.title"),
102 spacer(),
103 button("Reset").secondary(),
104 button("Save changes").primary(),
105 ])
106 .key("metric:header")
107 .gap(tokens::SPACE_3)
108 .height(Size::Fixed(56.0))
109 .padding(Sides::xy(tokens::SPACE_4, 0.0))
110 .align(Align::Center)
111 .stroke(tokens::BORDER)
112}
113
114fn settings_nav_card() -> El {
115 column([
116 settings_nav_item("Account", true),
117 settings_nav_item("Security", false),
118 settings_nav_item("Notifications", false),
119 settings_nav_item("Appearance", false),
120 settings_nav_item("Billing", false),
121 ])
122 .gap(tokens::SPACE_1)
123 .padding(tokens::SPACE_1)
124 .width(Size::Fixed(220.0))
125 .height(Size::Fill(1.0))
126 .style_profile(StyleProfile::Surface)
127 .surface_role(SurfaceRole::Panel)
128 .fill(tokens::CARD)
129 .stroke(tokens::BORDER)
130 .radius(tokens::RADIUS_MD)
131 .shadow(tokens::SHADOW_MD)
132}
133
134fn settings_nav_item(label: &'static str, selected: bool) -> El {
135 let mut item = row([
136 El::new(Kind::Custom("nav-dot"))
137 .fill(tokens::MUTED_FOREGROUND)
138 .radius(tokens::RADIUS_PILL)
139 .width(Size::Fixed(6.0))
140 .height(Size::Fixed(6.0)),
141 text(label)
142 .font_weight(FontWeight::Medium)
143 .ellipsis()
144 .width(Size::Fill(1.0)),
145 ])
146 .key(if selected {
147 "metric:settings.nav.row".to_string()
148 } else {
149 format!("settings-nav-{label}")
150 })
151 .metrics_role(MetricsRole::ListItem)
152 .align(Align::Center)
153 .focusable();
154
155 if selected {
156 item = item.current();
157 } else {
158 item = item.color(tokens::MUTED_FOREGROUND);
159 }
160
161 item
162}
163
164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}
185
186fn profile_card() -> El {
187 card([
188 card_header([
189 card_title("Profile"),
190 card_description("This information appears in audit logs and shared documents."),
191 ]),
192 card_content([form([
193 row([
194 setting_field("Display name", "Alicia Koch", "display-name"),
195 setting_field("Email", "alicia@acme.co", "email"),
196 ])
197 .gap(tokens::SPACE_3),
198 row([
199 setting_select("Role", "Workspace admin", "role"),
200 setting_select("Region", "US East", "region"),
201 ])
202 .gap(tokens::SPACE_3),
203 ])]),
204 ])
205 .key("metric:profile.card")
206}
207
208fn setting_field(label: &'static str, value: &'static str, key: &'static str) -> El {
209 form_item([
210 form_label(label),
211 form_control(
212 text_input(value, &Selection::caret(key, value.len()), key).key(
213 if key == "display-name" {
214 "metric:form.input"
215 } else {
216 key
217 },
218 ),
219 ),
220 ])
221 .width(Size::Fill(1.0))
222}
223
224fn setting_select(label: &'static str, value: &'static str, key: &'static str) -> El {
225 form_item([form_label(label), form_control(select_trigger(key, value))]).width(Size::Fill(1.0))
226}
227
228fn preferences_card() -> El {
229 card([
230 card_header([
231 card_title("Preferences"),
232 card_description("Defaults used when creating new dashboards and exports."),
233 ]),
234 card_content([column([
235 preference_row(
236 "Compact navigation",
237 "Use tighter rows in the sidebar and command menus.",
238 switch(true).key("compact-navigation"),
239 ),
240 divider(),
241 preference_row(
242 "Email summaries",
243 "Send a daily digest when documents change.",
244 switch(false).key("email-summaries"),
245 ),
246 divider(),
247 preference_row(
248 "Require approval",
249 "Route external sharing through an owner review.",
250 checkbox(true).key("approval-required"),
251 ),
252 ])
253 .gap(0.0)
254 .width(Size::Fill(1.0))])
255 .padding(0.0),
256 ])
257 .key("metric:preferences.card")
258}
259
260fn preference_row(title: &'static str, description: &'static str, control: El) -> El {
261 row([
262 column([
263 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
264 text(description)
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0))
271 .height(Size::Hug),
272 control,
273 ])
274 .key(if title == "Compact navigation" {
275 "metric:preference.row".to_string()
276 } else {
277 format!("preference-{title}")
278 })
279 .metrics_role(MetricsRole::PreferenceRow)
280 .gap(tokens::SPACE_4)
281 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_3))
282 .align(Align::Center)
283}
284
285fn settings_aside() -> El {
286 column([security_card(), scale_card()])
287 .gap(tokens::SPACE_4)
288 .width(Size::Fixed(300.0))
289 .height(Size::Fill(1.0))
290}
291
292fn security_card() -> El {
293 card([
294 card_header([
295 card_title("Security"),
296 card_description("Two-factor authentication is enabled for all privileged users."),
297 ]),
298 card_content([
299 compact_stat("Passkeys", "2 registered", badge("On").success()),
300 compact_stat("Sessions", "3 active", button("Review").secondary()),
301 ]),
302 ])
303 .width(Size::Fill(1.0))
304}
305
306fn scale_card() -> El {
307 card([
308 card_header([
309 card_title("Interface scale"),
310 card_description("Reference captures keep browser zoom fixed and vary root UI scale."),
311 ]),
312 card_content([
313 row([text("Dense").caption(), spacer(), text("Default").caption()]),
314 slider(0.66, tokens::PRIMARY)
315 .key("interface-scale")
316 .width(Size::Fill(1.0)),
317 ]),
318 ])
319 .width(Size::Fill(1.0))
320}
321
322fn compact_stat(title: &'static str, detail: &'static str, control: El) -> El {
323 row([
324 column([
325 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
326 text(detail).caption().ellipsis().width(Size::Fill(1.0)),
327 ])
328 .gap(2.0)
329 .width(Size::Fill(1.0))
330 .height(Size::Hug),
331 control,
332 ])
333 .gap(tokens::SPACE_2)
334 .height(Size::Fixed(44.0))
335 .align(Align::Center)
336}
337
338fn section_label(label: &'static str) -> El {
339 text(label)
340 .caption()
341 .height(Size::Fixed(22.0))
342 .padding(Sides::xy(tokens::SPACE_2, 0.0))
343}
344
345fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
346 let mut item = row([
347 icon(icon_name)
348 .color(tokens::MUTED_FOREGROUND)
349 .icon_size(tokens::ICON_SM)
350 .width(Size::Fixed(tokens::ICON_SM)),
351 text(label)
352 .font_weight(FontWeight::Medium)
353 .ellipsis()
354 .width(Size::Fill(1.0)),
355 ])
356 .key(if selected {
357 "metric:sidebar.nav.row".to_string()
358 } else {
359 format!("side-item-{label}")
360 })
361 .metrics_role(MetricsRole::ListItem)
362 .gap(tokens::SPACE_2)
363 .padding(Sides::xy(tokens::SPACE_2, 0.0))
364 .height(Size::Fixed(32.0))
365 .align(Align::Center)
366 .focusable();
367
368 if selected {
369 item = item.current();
370 } else {
371 item = item.color(tokens::MUTED_FOREGROUND);
372 }
373
374 item
375}
376
377fn icon_slot(icon_name: &'static str) -> El {
378 El::new(Kind::Custom("icon_cell"))
379 .style_profile(StyleProfile::Surface)
380 .child(
381 icon(icon_name)
382 .color(tokens::FOREGROUND)
383 .icon_size(tokens::ICON_XS),
384 )
385 .align(Align::Center)
386 .justify(Justify::Center)
387 .fill(tokens::MUTED)
388 .stroke(tokens::BORDER)
389 .radius(tokens::RADIUS_SM)
390 .width(Size::Fixed(30.0))
391 .height(Size::Fixed(30.0))
392}35fn fixture() -> El {
36 column([
37 h2("Scrollbar"),
38 text("scroll() and virtual_list() show a draggable thumb by default.").muted(),
39 row([
40 // 1) scroll() — default-on scrollbar.
41 column([
42 text("scroll() — default").bold(),
43 scroll(list_rows())
44 .height(Size::Fixed(240.0))
45 .padding(tokens::SPACE_2)
46 .stroke(tokens::BORDER)
47 .stroke_width(1.0)
48 .radius(tokens::RADIUS_MD),
49 ])
50 .gap(tokens::SPACE_2)
51 .width(Size::Fill(1.0))
52 .height(Size::Hug),
53 // 2) virtual_list — thumb scales to content size.
54 column([
55 text("virtual_list(200, 28)").bold(),
56 virtual_list(200, 28.0, |i| {
57 row([
58 text(format!("{i:03}")).mono().muted(),
59 text(format!("row {i}")),
60 ])
61 .gap(tokens::SPACE_2)
62 .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_1))
63 .height(Size::Fixed(28.0))
64 .align(Align::Center)
65 })
66 .height(Size::Fixed(240.0))
67 .padding(tokens::SPACE_2)
68 .stroke(tokens::BORDER)
69 .stroke_width(1.0)
70 .radius(tokens::RADIUS_MD),
71 ])
72 .gap(tokens::SPACE_2)
73 .width(Size::Fill(1.0))
74 .height(Size::Hug),
75 // 3) Opt-out: same content, no thumb.
76 column([
77 text("scroll().no_scrollbar()").bold(),
78 scroll(list_rows())
79 .no_scrollbar()
80 .height(Size::Fixed(240.0))
81 .padding(tokens::SPACE_2)
82 .stroke(tokens::BORDER)
83 .stroke_width(1.0)
84 .radius(tokens::RADIUS_MD),
85 ])
86 .gap(tokens::SPACE_2)
87 .width(Size::Fill(1.0))
88 .height(Size::Hug),
89 ])
90 .gap(tokens::SPACE_4)
91 .width(Size::Fill(1.0)),
92 ])
93 .gap(tokens::SPACE_4)
94 .padding(tokens::SPACE_7)
95}Sourcepub fn shadow(self, s: f32) -> Self
pub fn shadow(self, s: f32) -> Self
Examples found in repository?
114fn settings_nav_card() -> El {
115 column([
116 settings_nav_item("Account", true),
117 settings_nav_item("Security", false),
118 settings_nav_item("Notifications", false),
119 settings_nav_item("Appearance", false),
120 settings_nav_item("Billing", false),
121 ])
122 .gap(tokens::SPACE_1)
123 .padding(tokens::SPACE_1)
124 .width(Size::Fixed(220.0))
125 .height(Size::Fill(1.0))
126 .style_profile(StyleProfile::Surface)
127 .surface_role(SurfaceRole::Panel)
128 .fill(tokens::CARD)
129 .stroke(tokens::BORDER)
130 .radius(tokens::RADIUS_MD)
131 .shadow(tokens::SHADOW_MD)
132}Sourcepub fn surface_role(self, role: SurfaceRole) -> Self
pub fn surface_role(self, role: SurfaceRole) -> Self
Tag this node with a semantic SurfaceRole so the theme can
route it through the appropriate paint recipe. Most app code
should not call this directly: the catalog widgets (card(),
sidebar(), dialog(), popover(), tabs_list(), etc.) set
the right role and the matching fill / stroke / radius /
shadow together, while the .selected() and .current()
chainables wrap the corresponding state recipes.
Reach for the raw chainable when authoring a new widget or when
composing a custom container that the catalog doesn’t cover —
and remember that decorative roles (Panel, Raised, Popover,
Danger) require you to supply a fill yourself; see the
SurfaceRole doc for the per-variant contract. The bundle
lint pass flags Panel without a fill as
crate::bundle::lint::FindingKind::MissingSurfaceFill.
Examples found in repository?
114fn settings_nav_card() -> El {
115 column([
116 settings_nav_item("Account", true),
117 settings_nav_item("Security", false),
118 settings_nav_item("Notifications", false),
119 settings_nav_item("Appearance", false),
120 settings_nav_item("Billing", false),
121 ])
122 .gap(tokens::SPACE_1)
123 .padding(tokens::SPACE_1)
124 .width(Size::Fixed(220.0))
125 .height(Size::Fill(1.0))
126 .style_profile(StyleProfile::Surface)
127 .surface_role(SurfaceRole::Panel)
128 .fill(tokens::CARD)
129 .stroke(tokens::BORDER)
130 .radius(tokens::RADIUS_MD)
131 .shadow(tokens::SHADOW_MD)
132}Sourcepub fn paint_overflow(self, outset: impl Into<Sides>) -> Self
pub fn paint_overflow(self, outset: impl Into<Sides>) -> Self
Permit paint to extend beyond this element’s layout bounds by
outset on each side. Layout-neutral; siblings don’t move and
hit-testing still uses the layout rect.
Sourcepub fn focus_ring_inside(self) -> Self
pub fn focus_ring_inside(self) -> Self
Draw the stock focus ring just inside this node’s layout rect.
The default focus ring is outside the rect so it does not reduce usable control area. Inside rings are for dense, flush stacks such as menu rows, where adding gaps would change the intended visual recipe.
Sourcepub fn focus_ring_outside(self) -> Self
pub fn focus_ring_outside(self) -> Self
Draw the stock focus ring outside this node’s layout rect.
Sourcepub fn tooltip(self, text: impl Into<String>) -> Self
pub fn tooltip(self, text: impl Into<String>) -> Self
Attach a hover tooltip to this element. The runtime synthesizes a floating tooltip layer when the pointer rests on the node for the configured delay.
The node must also have a key. Tooltips fire
through the hit-test pipeline, and crate::hit_test only
returns keyed nodes — an unkeyed leaf with .tooltip() is
silently dead, because hover skips past it to the nearest
keyed ancestor (which has a different computed_id and a
different tooltip). The bundle lint flags this case as
crate::bundle::lint::FindingKind::DeadTooltip.
For info-only chrome inside list rows (sha cells, timestamps,
chips, identicon avatars) the usual key is a synthetic one
like "row:{idx}.<part>" — its only purpose is to make the
tooltip’s hover land. The tooltip text is snapshotted onto the
hit target at hit-test time, so tooltips fire correctly even
on virtual_list_dyn rows whose children are realized only
during layout.
Sourcepub fn cursor(self, cursor: Cursor) -> Self
pub fn cursor(self, cursor: Cursor) -> Self
Declare the pointer cursor when the pointer is over this element.
Sourcepub fn cursor_pressed(self, cursor: Cursor) -> Self
pub fn cursor_pressed(self, cursor: Cursor) -> Self
Declare the cursor shown only while a press is captured at this exact node.
Sourcepub fn opacity(self, v: f32) -> Self
pub fn opacity(self, v: f32) -> Self
Multiply this element’s paint alpha by v (clamped to [0, 1]).
Sourcepub fn translate(self, x: f32, y: f32) -> Self
pub fn translate(self, x: f32, y: f32) -> Self
Offset this element’s paint and its descendants by (x, y) in
logical pixels.
Sourcepub fn scale(self, v: f32) -> Self
pub fn scale(self, v: f32) -> Self
Uniformly scale this element’s paint around its rect centre.
Sourcepub fn animate(self, timing: Timing) -> Self
pub fn animate(self, timing: Timing) -> Self
Opt this element into app-driven prop interpolation.
Sourcepub fn shader(self, binding: ShaderBinding) -> Self
pub fn shader(self, binding: ShaderBinding) -> Self
Bind a shader for the surface paint, replacing the implicit
stock::rounded_rect.
Examples found in repository?
More examples
44fn graph_cell(lane: u8, selected: bool) -> El {
45 let lane_color = lane_palette(lane);
46 let ring_color = if selected {
47 Color::rgb(245, 245, 250)
48 } else {
49 lane_color
50 };
51 let ring_w = if selected { 2.5 } else { 1.5 };
52 let radius = 5.0;
53 let line_w = 2.0;
54 let lane_frac = (lane as f32 + 0.5) / LANE_COUNT as f32;
55
56 El::new(Kind::Custom("graph_cell"))
57 .width(Size::Fixed(GRAPH_WIDTH))
58 .height(Size::Fixed(ROW_HEIGHT))
59 .shader(
60 ShaderBinding::custom("commit_node")
61 .color("vec_a", tokens::BACKGROUND)
62 .color("vec_b", ring_color)
63 .vec4("vec_c", [radius, ring_w, line_w, lane_frac]),
64 )
65 .fill(lane_color)
66}Sourcepub fn style_profile(self, p: StyleProfile) -> Self
pub fn style_profile(self, p: StyleProfile) -> Self
Examples found in repository?
451fn icon_cell(label: &'static str) -> El {
452 El::new(Kind::Custom("icon_cell"))
453 .style_profile(StyleProfile::Surface)
454 .text(label)
455 .text_align(TextAlign::Center)
456 .caption()
457 .font_weight(FontWeight::Semibold)
458 .fill(tokens::MUTED)
459 .radius(tokens::RADIUS_SM)
460 .width(Size::Fixed(30.0))
461 .height(Size::Fixed(30.0))
462}More examples
355fn icon_cell(label: &'static str) -> El {
356 El::new(Kind::Custom("icon_cell"))
357 .style_profile(StyleProfile::Surface)
358 .text(label)
359 .text_align(TextAlign::Center)
360 .caption()
361 .font_weight(FontWeight::Semibold)
362 .fill(tokens::MUTED)
363 .stroke(tokens::BORDER)
364 .radius(tokens::RADIUS_SM)
365 .width(Size::Fixed(26.0))
366 .height(Size::Fixed(26.0))
367}114fn settings_nav_card() -> El {
115 column([
116 settings_nav_item("Account", true),
117 settings_nav_item("Security", false),
118 settings_nav_item("Notifications", false),
119 settings_nav_item("Appearance", false),
120 settings_nav_item("Billing", false),
121 ])
122 .gap(tokens::SPACE_1)
123 .padding(tokens::SPACE_1)
124 .width(Size::Fixed(220.0))
125 .height(Size::Fill(1.0))
126 .style_profile(StyleProfile::Surface)
127 .surface_role(SurfaceRole::Panel)
128 .fill(tokens::CARD)
129 .stroke(tokens::BORDER)
130 .radius(tokens::RADIUS_MD)
131 .shadow(tokens::SHADOW_MD)
132}
133
134fn settings_nav_item(label: &'static str, selected: bool) -> El {
135 let mut item = row([
136 El::new(Kind::Custom("nav-dot"))
137 .fill(tokens::MUTED_FOREGROUND)
138 .radius(tokens::RADIUS_PILL)
139 .width(Size::Fixed(6.0))
140 .height(Size::Fixed(6.0)),
141 text(label)
142 .font_weight(FontWeight::Medium)
143 .ellipsis()
144 .width(Size::Fill(1.0)),
145 ])
146 .key(if selected {
147 "metric:settings.nav.row".to_string()
148 } else {
149 format!("settings-nav-{label}")
150 })
151 .metrics_role(MetricsRole::ListItem)
152 .align(Align::Center)
153 .focusable();
154
155 if selected {
156 item = item.current();
157 } else {
158 item = item.color(tokens::MUTED_FOREGROUND);
159 }
160
161 item
162}
163
164fn settings_body() -> El {
165 column([
166 column([
167 h1("Account").heading().key("metric:section.title"),
168 text("Manage identity, workspace defaults, and security preferences.")
169 .muted()
170 .wrap_text()
171 .key("metric:page.subtitle"),
172 ])
173 .gap(tokens::SPACE_1)
174 .height(Size::Hug),
175 scroll([profile_card(), preferences_card()])
176 .key("settings-body-scroll")
177 .gap(tokens::SPACE_4)
178 .width(Size::Fill(1.0))
179 .height(Size::Fill(1.0)),
180 ])
181 .gap(tokens::SPACE_4)
182 .width(Size::Fill(1.0))
183 .height(Size::Fill(1.0))
184}
185
186fn profile_card() -> El {
187 card([
188 card_header([
189 card_title("Profile"),
190 card_description("This information appears in audit logs and shared documents."),
191 ]),
192 card_content([form([
193 row([
194 setting_field("Display name", "Alicia Koch", "display-name"),
195 setting_field("Email", "alicia@acme.co", "email"),
196 ])
197 .gap(tokens::SPACE_3),
198 row([
199 setting_select("Role", "Workspace admin", "role"),
200 setting_select("Region", "US East", "region"),
201 ])
202 .gap(tokens::SPACE_3),
203 ])]),
204 ])
205 .key("metric:profile.card")
206}
207
208fn setting_field(label: &'static str, value: &'static str, key: &'static str) -> El {
209 form_item([
210 form_label(label),
211 form_control(
212 text_input(value, &Selection::caret(key, value.len()), key).key(
213 if key == "display-name" {
214 "metric:form.input"
215 } else {
216 key
217 },
218 ),
219 ),
220 ])
221 .width(Size::Fill(1.0))
222}
223
224fn setting_select(label: &'static str, value: &'static str, key: &'static str) -> El {
225 form_item([form_label(label), form_control(select_trigger(key, value))]).width(Size::Fill(1.0))
226}
227
228fn preferences_card() -> El {
229 card([
230 card_header([
231 card_title("Preferences"),
232 card_description("Defaults used when creating new dashboards and exports."),
233 ]),
234 card_content([column([
235 preference_row(
236 "Compact navigation",
237 "Use tighter rows in the sidebar and command menus.",
238 switch(true).key("compact-navigation"),
239 ),
240 divider(),
241 preference_row(
242 "Email summaries",
243 "Send a daily digest when documents change.",
244 switch(false).key("email-summaries"),
245 ),
246 divider(),
247 preference_row(
248 "Require approval",
249 "Route external sharing through an owner review.",
250 checkbox(true).key("approval-required"),
251 ),
252 ])
253 .gap(0.0)
254 .width(Size::Fill(1.0))])
255 .padding(0.0),
256 ])
257 .key("metric:preferences.card")
258}
259
260fn preference_row(title: &'static str, description: &'static str, control: El) -> El {
261 row([
262 column([
263 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
264 text(description)
265 .caption()
266 .ellipsis()
267 .width(Size::Fill(1.0)),
268 ])
269 .gap(2.0)
270 .width(Size::Fill(1.0))
271 .height(Size::Hug),
272 control,
273 ])
274 .key(if title == "Compact navigation" {
275 "metric:preference.row".to_string()
276 } else {
277 format!("preference-{title}")
278 })
279 .metrics_role(MetricsRole::PreferenceRow)
280 .gap(tokens::SPACE_4)
281 .padding(Sides::xy(tokens::SPACE_4, tokens::SPACE_3))
282 .align(Align::Center)
283}
284
285fn settings_aside() -> El {
286 column([security_card(), scale_card()])
287 .gap(tokens::SPACE_4)
288 .width(Size::Fixed(300.0))
289 .height(Size::Fill(1.0))
290}
291
292fn security_card() -> El {
293 card([
294 card_header([
295 card_title("Security"),
296 card_description("Two-factor authentication is enabled for all privileged users."),
297 ]),
298 card_content([
299 compact_stat("Passkeys", "2 registered", badge("On").success()),
300 compact_stat("Sessions", "3 active", button("Review").secondary()),
301 ]),
302 ])
303 .width(Size::Fill(1.0))
304}
305
306fn scale_card() -> El {
307 card([
308 card_header([
309 card_title("Interface scale"),
310 card_description("Reference captures keep browser zoom fixed and vary root UI scale."),
311 ]),
312 card_content([
313 row([text("Dense").caption(), spacer(), text("Default").caption()]),
314 slider(0.66, tokens::PRIMARY)
315 .key("interface-scale")
316 .width(Size::Fill(1.0)),
317 ]),
318 ])
319 .width(Size::Fill(1.0))
320}
321
322fn compact_stat(title: &'static str, detail: &'static str, control: El) -> El {
323 row([
324 column([
325 text(title).semibold().ellipsis().width(Size::Fill(1.0)),
326 text(detail).caption().ellipsis().width(Size::Fill(1.0)),
327 ])
328 .gap(2.0)
329 .width(Size::Fill(1.0))
330 .height(Size::Hug),
331 control,
332 ])
333 .gap(tokens::SPACE_2)
334 .height(Size::Fixed(44.0))
335 .align(Align::Center)
336}
337
338fn section_label(label: &'static str) -> El {
339 text(label)
340 .caption()
341 .height(Size::Fixed(22.0))
342 .padding(Sides::xy(tokens::SPACE_2, 0.0))
343}
344
345fn side_item(icon_name: &'static str, label: &'static str, selected: bool) -> El {
346 let mut item = row([
347 icon(icon_name)
348 .color(tokens::MUTED_FOREGROUND)
349 .icon_size(tokens::ICON_SM)
350 .width(Size::Fixed(tokens::ICON_SM)),
351 text(label)
352 .font_weight(FontWeight::Medium)
353 .ellipsis()
354 .width(Size::Fill(1.0)),
355 ])
356 .key(if selected {
357 "metric:sidebar.nav.row".to_string()
358 } else {
359 format!("side-item-{label}")
360 })
361 .metrics_role(MetricsRole::ListItem)
362 .gap(tokens::SPACE_2)
363 .padding(Sides::xy(tokens::SPACE_2, 0.0))
364 .height(Size::Fixed(32.0))
365 .align(Align::Center)
366 .focusable();
367
368 if selected {
369 item = item.current();
370 } else {
371 item = item.color(tokens::MUTED_FOREGROUND);
372 }
373
374 item
375}
376
377fn icon_slot(icon_name: &'static str) -> El {
378 El::new(Kind::Custom("icon_cell"))
379 .style_profile(StyleProfile::Surface)
380 .child(
381 icon(icon_name)
382 .color(tokens::FOREGROUND)
383 .icon_size(tokens::ICON_XS),
384 )
385 .align(Align::Center)
386 .justify(Justify::Center)
387 .fill(tokens::MUTED)
388 .stroke(tokens::BORDER)
389 .radius(tokens::RADIUS_SM)
390 .width(Size::Fixed(30.0))
391 .height(Size::Fixed(30.0))
392}Trait Implementations§
Auto Trait Implementations§
impl Freeze for El
impl !RefUnwindSafe for El
impl Send for El
impl Sync for El
impl Unpin for El
impl UnsafeUnpin for El
impl !UnwindSafe for El
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<SS, SP> SupersetOf<SS> for SPwhere
SS: SubsetOf<SP>,
impl<SS, SP> SupersetOf<SS> for SPwhere
SS: SubsetOf<SP>,
Source§fn to_subset(&self) -> Option<SS>
fn to_subset(&self) -> Option<SS>
self from the equivalent element of its
superset. Read moreSource§fn is_in_subset(&self) -> bool
fn is_in_subset(&self) -> bool
self is actually part of its subset T (and can be converted to it).Source§fn to_subset_unchecked(&self) -> SS
fn to_subset_unchecked(&self) -> SS
self.to_subset but without any property checks. Always succeeds.Source§fn from_subset(element: &SS) -> SP
fn from_subset(element: &SS) -> SP
self to the equivalent element of its superset.