Showcase
Specular authors mix markdown prose, plain Scala / ZIO values, and ascent UI in the
same DocSpec.
This page is the power-user tour: headings, lists, quotes, tables, value examples, and CSS-in-Scala layouts, all from one source that also runs as tests. For the adoption story and wiring, start at Why Specular and Getting started.
Markdown palette
Inline styles
Emphasis with italics, bold, and inline code. Link out to ascent.
Lists
Prose via
md"..."→ commonmark → ascentUIUI examples via
example/exampleIOwith source captureValues and effects via
exampleValue/exampleZIO(sameValueExamplenode)Interactive mounts via
.interactive
Write a
DocSpecRun
sbt testBuild the site
Quote
Same AST, two interpreters: tests keep docs honest; the site shows them.
Table
| Construct | Where it lives | Output |
|---|---|---|
md"..." | Prose | SSR HTML |
example | Example | source + UI snap |
exampleValue / exampleZIO | ValueExample | source + result |
expectFail | FailExample | source + diagnostics |
expectCrash | CrashExample | source + failure |
.assert | zio-test bridge | CI green/red |
.interactive | client registry | live mount |
Raw HTML in markdown is dropped (no XSS footgun):
Plain Scala
Not every docs example is a UI. exampleValue captures a plain expression: source panel plus
the printed result. Assert the value the same way you would in zio-test.
{
val xs = List(1, 2, 3, 4)
xs.filter(_ % 2 == 0).sum
}6Plain ZIO
Effects use the same ValueExample node: exampleZIO stores a success-typed URIO. Site and
tests run the body under Scope and print / assert the result.
for
a <- ZIO.succeed(21)
b <- ZIO.succeed(2)
yield a * b42Compile-fail examples
Guide pages that teach what must not compile use expectFail with a self-contained string
snippet (Saferis / zio-test typeCheckErrors style). The site shows the source and the real
diagnostics; .assert fails CI if the snippet unexpectedly typechecks.
val x: Int = "nope"Found: ("nope" : String)
Required: Int
val x: Int = "nope"Runtime-fail examples
expectCrash captures a fallible effect (not URIO). The site renders the source plus the
pretty-printed failure; tests fail if the effect succeeds.
ZIO.fail(new IllegalArgumentException("demo failure")): ZIO[Scope, Throwable, Nothing]java.lang.IllegalArgumentException: demo failure
at specular.docs.Showcase$.doc$$anonfun$7(Showcase.scala:97)
at zio.ZIO$.fail$$anonfun$1(ZIO.scala:3251)
at zio.ZIO$.failCause$$anonfun$1(ZIO.scala:3262)
at specular.docs.Showcase.doc(Showcase.scala:97)
at specular.site.SiteBuilder.Live.renderNode(SiteBuilder.scala:294)
at specular.site.SiteBuilder.Live.renderNode(SiteBuilder.scala:322)
at specular.site.SiteBuilder.Live.renderNodes(SiteBuilder.scala:190)
at specular.site.SiteBuilder.Live.renderNodes(SiteBuilder.scala:194)
at specular.site.SiteBuilder.Live.renderNode(SiteBuilder.scala:227)
at specular.site.SiteBuilder.Live.renderNodes(SiteBuilder.scala:190)
at specular.site.SiteBuilder.Live.renderNodes(SiteBuilder.scala:194)
at specular.site.SiteBuilder.Live.renderPageBody(SiteBuilder.scala:110)
at specular.site.SiteBuilder.Live.renderOne(SiteBuilder.scala:97)
at specular.site.SiteBuilder.Live.buildSite(SiteBuilder.scala:58)
at specular.site.SiteBuilder.Live.buildSite(SiteBuilder.scala:63)
at specular.site.DocsSite.build(DocsSite.scala:68)
at specular.site.DocsSite.build(DocsSite.scala:69)
at specular.site.DocsSite.run(DocsSite.scala:74)
CSS-in-Scala layouts
Examples are real ascent trees: define CssClasses with the typed S catalog, then apply them like any other attr:
{
val ink = Color.hex("#1a1a1a")
val accent = Color.hex("#0b5fff")
object Callout
extends CssClass(
S.padding(1.rem, 1.25.rem),
S.borderLeft(Border.solid(4.px, accent)),
S.background(Color.hex("#eef4ff")),
S.color(ink),
S.borderRadius(0.px, 8.px, 8.px, 0.px),
S.margin(0.75.rem, 0.px),
)
object Badge
extends CssClass(
S.display.inlineBlock,
S.padding(0.15.rem, 0.55.rem),
S.fontSize(0.75.rem),
S.fontWeight(600),
S.letterSpacing(0.04.em),
S.textTransform.uppercase,
S.color(accent),
S.background(Color.hex("#e8f0ff")),
S.borderRadius.px(999),
)
E.div(
Callout,
E.span(Badge, "tip"),
E.p("Callouts, badges, and cards are ordinary ", E.code("CssClass"), " values, not a separate docs DSL."),
)
}Callouts, badges, and cards are ordinary CssClass values, not a separate docs DSL.
{
val ink = Color.hex("#1a1a1a")
object Row
extends CssClass(
S.display.flex,
S.gap(1.rem),
S.flexWrap.wrap,
S.alignItems.stretch,
)
object Card
extends CssClass(
S.display.grid,
S.gap(0.5.rem),
S.padding(1.25.rem),
S.background(Color.hex("#ffffff")),
S.color(ink),
S.border(Border.solid(1.px, Color.hex("#e2e2e2"))),
S.borderRadius.px(10),
S.boxShadow(Shadow(0.px, 1.px, 2.px, Color.rgba(0, 0, 0, 0.04))),
)
E.div(
Row,
E.div(Card, E.h3("Tests"), E.p("Every ", E.code(".assert"), " example fails CI when it drifts.")),
E.div(Card, E.h3("Site"), E.p("SSR via ascent-html: same Mount engine as the browser.")),
E.div(Card, E.h3("Live"), E.p("Interactive examples remount into ", E.code("#<slug>-ex-*"), " wrappers.")),
)
}Tests
Every .assert example fails CI when it drifts.
Site
SSR via ascent-html: same Mount engine as the browser.
Live
Interactive examples remount into #<slug>-ex-* wrappers.
Interactive + styled
Combine CssClass with sq state: still one exampleIO block:
{
val ink = Color.hex("#1a1a1a")
val accent = Color.hex("#0b5fff")
object Card
extends CssClass(
S.display.grid,
S.gap(0.5.rem),
S.padding(1.25.rem),
S.background(Color.hex("#ffffff")),
S.color(ink),
S.border(Border.solid(1.px, Color.hex("#e2e2e2"))),
S.borderRadius.px(10),
S.boxShadow(Shadow(0.px, 1.px, 2.px, Color.rgba(0, 0, 0, 0.04))),
)
object AccentButton
extends CssClass(
S.padding(0.45.rem, 0.9.rem),
S.border.none,
S.borderRadius.px(6),
S.background(accent),
S.color(Color.hex("#ffffff")),
S.fontWeight(600),
S.cursor.pointer,
Selector(PseudoClass.hover, S.background(Color.hex("#094acc"))),
)
for on <- sq(false)
yield E.div(
Card,
E.p(on.map(v => if v then "Status: on" else "Status: off")),
E.button(
AccentButton,
Events.onClick(_ => on.update(!_)),
on.map(v => if v then "Turn off" else "Turn on"),
),
)
}Status: off