diff --git a/examples/01-hello/README.md b/examples/01-hello/README.md
index 16bedc73..840c64ee 100644
--- a/examples/01-hello/README.md
+++ b/examples/01-hello/README.md
@@ -1,17 +1,36 @@
-# Example 13 — ui.tsx hello world (no build step)
+# Example 01 — Old Faithful, `ui.tsx` style (no build step)
-The smallest possible `ui.tsx`-first Shiny app: a Python server that contains only reactive logic, plus a static React client served from `www/`. No JSX, no bundler, no `package.json`. Edit `app.js` and reload.
+Shiny's canonical [`01_hello`](https://github.com/rstudio/shiny/blob/main/inst/examples-shiny/01_hello/app.R)
+app — a bins slider over the Old Faithful waiting times — rebuilt as the
+smallest possible `ui.tsx`-first app. No JSX, no bundler, no `package.json`.
+Edit `app.js` and reload.
-`app.py` (Express, via `set_react_page()`) and `app-core.py` (Core, via `page_react_html()`) are two server-side entries for the same `www/` client.
+`app.py` (Express, via `set_react_page()`) and `app-core.py` (Core, via
+`page_react_html()`) are two server-side entries for the same `www/` client;
+`app.R` is the R twin.
## What it shows
-A name-input form and click counter rendered twice for direct comparison:
+The split that makes the pattern worth using. In traditional Shiny, `01_hello`
+renders the histogram *on the server* — `renderPlot({ hist(...) })` ships a PNG
+to the browser and the client is a passive `
`. Here the server never
+produces a picture:
-- **Client card** — `Hello, {name}!` and `Count: {clickCount}` computed locally in React state. Updates on every keystroke / click with no roundtrip.
-- **Server card** — same two values, but routed through Shiny: `useShinyInput("name")` → `@reactive.calc greeting` → `@reactive_output txtout_title` → `useShinyOutputValue("txtout_title")`. Updates lag by the websocket round-trip.
+- **Server** — one `reactive_output` returning `{breaks, counts}` (plain JSON
+ from R's `hist(..., plot = FALSE)` / a dependency-free Python binner), plus a
+ caption string. That's the entire server. No plotting library, no image
+ encoding, no `plotOutput` placeholder.
+- **Client** — `www/app.js` reads that JSON with `useShinyOutputValue` and draws
+ the bars as SVG ``s. Because the chart is real DOM the client owns, it
+ can be styled, animated, or made interactive without another round trip.
-The point is that the same data shows up on both cards but the latency is visibly different — the client card is instantaneous, the server card has the websocket delay you'd expect.
+The bins slider goes the other way: `useShinyInput("bins", 30)` pushes the value
+to Shiny, which recomputes the counts.
+
+It also demonstrates the output-status idiom from the repo's guidance — the
+chart stays mounted while the server recomputes and only dims via
+`useShinyOutputStatus("dist_data") === "recalculating"`. Dragging the slider
+never tears the SVG down and re-mounts it.
## Layout
@@ -19,19 +38,30 @@ The point is that the same data shows up on both cards but the latency is visibl
examples/01-hello/
├── app.py # Express: set_react_page() + 2 reactive_output outputs
├── app-core.py # Core: page_react_html() + App(app_ui, server), same outputs
+├── app.R # R: page_react_html() + reactive_output, same outputs
+├── faithful.py # Old Faithful waiting times + a stdlib-only binner (Python)
+├── faithful.csv # base R's `faithful` dataset, exported for the Python servers
└── www/
- ├── index.html # 3 lines: stylesheet, #root div, script
- ├── app.js # raw React.createElement (with `h` shorthand)
- └── main.css # body reset
+ ├── index.html # 2 lines: stylesheet, script (the app appends its own mount div to )
+ ├── app.js # raw React.createElement (with `h` shorthand) + an SVG histogram
+ └── main.css # sidebar/panel layout
```
-Five files. No `node_modules`, no Vite, no build script.
+No `node_modules`, no Vite, no build script — and on the Python side no
+numpy/matplotlib either.
+
+`app.R` uses base R's built-in `faithful` dataset; `faithful.csv` is that same
+data exported so the Python servers don't need a data dependency. R's outputs
+return `NULL` until the client's first `bins` message arrives (Python raises a
+silent exception instead), and wrap the histogram vectors in `I()` so a
+single-bin result still serializes as a JSON array rather than a scalar.
## Bridge primitives used
-- `from shinyreact import reactive_output, set_react_page` (Express server, `app.py`) / `page_react_html` (Core server, `app-core.py`)
-- `window.shinyreact.useShinyInput(id, default, options?)` for the name field and click counter
-- `window.shinyreact.useShinyOutputValue(id, default)` for the server-computed title and count
+- `from shinyreact import reactive_output, set_react_page` (Express server, `app.py`) / `page_react_html` (Core server, `app-core.py`); `library(shinyreact)` with `page_react_html()` + `reactive_output()` in `app.R`
+- `window.shinyreact.useShinyInput(id, default)` for the bins slider
+- `window.shinyreact.useShinyOutputValue(id, default)` for the histogram data and caption
+- `window.shinyreact.useShinyOutputStatus(id)` to dim the chart while it recalculates
- `window.shinyreact.useShinyInitialized()` to suppress the placeholder UI during connection setup
`window.shinyreact.React` and `window.shinyreact.ReactDOM` are pulled in directly so the React app shares the React instance that owns the shinyreact hooks.
@@ -54,3 +84,7 @@ Open the URL printed by Shiny.
## When to use this pattern
Good fit for `ui.tsx`-first apps that are small enough to not need JSX or component libraries — proof of concept, internal tools, anything where the cost of running a build is more than the cost of writing `React.createElement` calls. As soon as you want shadcn or Tailwind utility classes, see [03-columns-shadcn](../03-columns-shadcn/) and [04-shadcn](../04-shadcn/) for the Vite-based setup.
+
+If you'd rather keep rendering server-side, [04-shadcn](../04-shadcn/) shows
+`@render.plot` + `ImageOutput` (matplotlib PNGs) side by side with the data-only
+approach used here.
diff --git a/examples/01-hello/app-core.py b/examples/01-hello/app-core.py
index cd8d14f4..a96e7a06 100644
--- a/examples/01-hello/app-core.py
+++ b/examples/01-hello/app-core.py
@@ -1,22 +1,19 @@
-from shiny import App, Inputs, Outputs, Session, reactive
+from faithful import histogram, waiting
+from shiny import App, Inputs, Outputs, Session
from shinyreact import page_react_html, reactive_output
app_ui = page_react_html() # serves www/index.html (Core API)
def server(input: Inputs, output: Outputs, session: Session):
- @reactive.calc
- def greeting():
- name = input.name()
- return name if name else "World"
-
@reactive_output
- def txtout_title():
- return f"Hello, {greeting()}!"
+ def dist_data():
+ return histogram(waiting, input.bins())
@reactive_output
- def txtout_count():
- return input.click_count()
+ def dist_caption():
+ n = input.bins()
+ return f"{len(waiting)} eruptions in {n} bin{'' if n == 1 else 's'}"
app = App(app_ui, server)
diff --git a/examples/01-hello/app.R b/examples/01-hello/app.R
index b0fc0386..be9718fd 100644
--- a/examples/01-hello/app.R
+++ b/examples/01-hello/app.R
@@ -1,20 +1,42 @@
library(shiny)
library(shinyreact)
+# Base R ships the Old Faithful dataset; the Python servers read the same data
+# from the faithful.csv exported next to this file.
+waiting <- faithful$waiting
+
ui <- page_react_html("www/index.html")
server <- function(input, output, session) {
- greeting <- reactive({
- name <- input$name
- if (is.null(name) || nchar(name) == 0) "World" else name
- })
+ # input$bins is NULL until the client's first useShinyInput("bins", 30)
+ # message arrives. Returning NULL leaves the React side on its "Loading…"
+ # placeholder; req() would work too, but its silent error still reaches the
+ # client. (Python's input.bins() raises a silent exception instead.)
+ bins <- reactive(input$bins)
- output$txtout_title <- reactive_output({
- paste0("Hello, ", greeting(), "!")
+ output$dist_data <- reactive_output({
+ n <- bins()
+ if (is.null(n)) {
+ return(NULL)
+ }
+ breaks <- seq(min(waiting), max(waiting), length.out = n + 1)
+ h <- hist(waiting, breaks = breaks, plot = FALSE)
+ # I() keeps length-1 vectors as JSON arrays (n = 1) instead of scalars.
+ list(breaks = I(h$breaks), counts = I(h$counts))
})
- output$txtout_count <- reactive_output({
- input$click_count
+ output$dist_caption <- reactive_output({
+ n <- bins()
+ if (is.null(n)) {
+ return(NULL)
+ }
+ paste0(
+ length(waiting),
+ " eruptions in ",
+ n,
+ " bin",
+ if (n == 1) "" else "s"
+ )
})
}
diff --git a/examples/01-hello/app.py b/examples/01-hello/app.py
index 574670bd..ab8d2359 100644
--- a/examples/01-hello/app.py
+++ b/examples/01-hello/app.py
@@ -1,23 +1,16 @@
-from shiny import reactive
+from faithful import histogram, waiting
from shiny.express import input
from shinyreact import reactive_output, set_react_page
set_react_page()
-@reactive.calc
-def greeting():
- name = input.name()
- if not name:
- return "World"
- return name
-
-
@reactive_output
-def txtout_title():
- return f"Hello, {greeting()}!"
+def dist_data():
+ return histogram(waiting, input.bins())
@reactive_output
-def txtout_count():
- return input.click_count()
+def dist_caption():
+ n = input.bins()
+ return f"{len(waiting)} eruptions in {n} bin{'' if n == 1 else 's'}"
diff --git a/examples/01-hello/faithful.csv b/examples/01-hello/faithful.csv
new file mode 100644
index 00000000..2f23dc97
--- /dev/null
+++ b/examples/01-hello/faithful.csv
@@ -0,0 +1,273 @@
+"eruptions","waiting"
+3.6,79
+1.8,54
+3.333,74
+2.283,62
+4.533,85
+2.883,55
+4.7,88
+3.6,85
+1.95,51
+4.35,85
+1.833,54
+3.917,84
+4.2,78
+1.75,47
+4.7,83
+2.167,52
+1.75,62
+4.8,84
+1.6,52
+4.25,79
+1.8,51
+1.75,47
+3.45,78
+3.067,69
+4.533,74
+3.6,83
+1.967,55
+4.083,76
+3.85,78
+4.433,79
+4.3,73
+4.467,77
+3.367,66
+4.033,80
+3.833,74
+2.017,52
+1.867,48
+4.833,80
+1.833,59
+4.783,90
+4.35,80
+1.883,58
+4.567,84
+1.75,58
+4.533,73
+3.317,83
+3.833,64
+2.1,53
+4.633,82
+2,59
+4.8,75
+4.716,90
+1.833,54
+4.833,80
+1.733,54
+4.883,83
+3.717,71
+1.667,64
+4.567,77
+4.317,81
+2.233,59
+4.5,84
+1.75,48
+4.8,82
+1.817,60
+4.4,92
+4.167,78
+4.7,78
+2.067,65
+4.7,73
+4.033,82
+1.967,56
+4.5,79
+4,71
+1.983,62
+5.067,76
+2.017,60
+4.567,78
+3.883,76
+3.6,83
+4.133,75
+4.333,82
+4.1,70
+2.633,65
+4.067,73
+4.933,88
+3.95,76
+4.517,80
+2.167,48
+4,86
+2.2,60
+4.333,90
+1.867,50
+4.817,78
+1.833,63
+4.3,72
+4.667,84
+3.75,75
+1.867,51
+4.9,82
+2.483,62
+4.367,88
+2.1,49
+4.5,83
+4.05,81
+1.867,47
+4.7,84
+1.783,52
+4.85,86
+3.683,81
+4.733,75
+2.3,59
+4.9,89
+4.417,79
+1.7,59
+4.633,81
+2.317,50
+4.6,85
+1.817,59
+4.417,87
+2.617,53
+4.067,69
+4.25,77
+1.967,56
+4.6,88
+3.767,81
+1.917,45
+4.5,82
+2.267,55
+4.65,90
+1.867,45
+4.167,83
+2.8,56
+4.333,89
+1.833,46
+4.383,82
+1.883,51
+4.933,86
+2.033,53
+3.733,79
+4.233,81
+2.233,60
+4.533,82
+4.817,77
+4.333,76
+1.983,59
+4.633,80
+2.017,49
+5.1,96
+1.8,53
+5.033,77
+4,77
+2.4,65
+4.6,81
+3.567,71
+4,70
+4.5,81
+4.083,93
+1.8,53
+3.967,89
+2.2,45
+4.15,86
+2,58
+3.833,78
+3.5,66
+4.583,76
+2.367,63
+5,88
+1.933,52
+4.617,93
+1.917,49
+2.083,57
+4.583,77
+3.333,68
+4.167,81
+4.333,81
+4.5,73
+2.417,50
+4,85
+4.167,74
+1.883,55
+4.583,77
+4.25,83
+3.767,83
+2.033,51
+4.433,78
+4.083,84
+1.833,46
+4.417,83
+2.183,55
+4.8,81
+1.833,57
+4.8,76
+4.1,84
+3.966,77
+4.233,81
+3.5,87
+4.366,77
+2.25,51
+4.667,78
+2.1,60
+4.35,82
+4.133,91
+1.867,53
+4.6,78
+1.783,46
+4.367,77
+3.85,84
+1.933,49
+4.5,83
+2.383,71
+4.7,80
+1.867,49
+3.833,75
+3.417,64
+4.233,76
+2.4,53
+4.8,94
+2,55
+4.15,76
+1.867,50
+4.267,82
+1.75,54
+4.483,75
+4,78
+4.117,79
+4.083,78
+4.267,78
+3.917,70
+4.55,79
+4.083,70
+2.417,54
+4.183,86
+2.217,50
+4.45,90
+1.883,54
+1.85,54
+4.283,77
+3.95,79
+2.333,64
+4.15,75
+2.35,47
+4.933,86
+2.9,63
+4.583,85
+3.833,82
+2.083,57
+4.367,82
+2.133,67
+4.35,74
+2.2,54
+4.45,83
+3.567,73
+4.5,73
+4.15,88
+3.817,80
+3.917,71
+4.45,83
+2,56
+4.283,79
+4.767,78
+4.533,84
+1.85,58
+4.25,83
+1.983,43
+2.25,60
+4.75,75
+4.117,81
+2.15,46
+4.417,90
+1.817,46
+4.467,74
diff --git a/examples/01-hello/faithful.py b/examples/01-hello/faithful.py
new file mode 100644
index 00000000..89c51754
--- /dev/null
+++ b/examples/01-hello/faithful.py
@@ -0,0 +1,27 @@
+"""Old Faithful waiting times + a dependency-free histogram binner.
+
+Shared by `app.py` (Express) and `app-core.py` (Core). R's `app.R` uses the
+`faithful` dataset built into base R and `hist(..., plot = FALSE)` instead;
+`faithful.csv` is that same dataset exported for the Python servers.
+"""
+
+from __future__ import annotations
+
+import csv
+from pathlib import Path
+
+_CSV = Path(__file__).parent / "faithful.csv"
+
+with _CSV.open(newline="") as f:
+ waiting: list[float] = [float(row["waiting"]) for row in csv.DictReader(f)]
+
+
+def histogram(values: list[float], bins: int) -> dict[str, list[float] | list[int]]:
+ """Equal-width binning matching R's `hist()`: bins are (lo, hi], first inclusive."""
+ lo, hi = min(values), max(values)
+ width = (hi - lo) / bins
+ breaks = [lo + i * width for i in range(bins + 1)]
+ counts = [0] * bins
+ for v in values:
+ counts[min(int((v - lo) / width), bins - 1)] += 1
+ return {"breaks": breaks, "counts": counts}
diff --git a/examples/01-hello/www/app.js b/examples/01-hello/www/app.js
index e44c79db..b5a85903 100644
--- a/examples/01-hello/www/app.js
+++ b/examples/01-hello/www/app.js
@@ -1,106 +1,191 @@
-const { React, ReactDOM, useShinyInput, useShinyOutputValue, useShinyInitialized } =
- window.shinyreact;
+const {
+ React,
+ ReactDOM,
+ useShinyInput,
+ useShinyOutputValue,
+ useShinyOutputStatus,
+ useShinyInitialized,
+} = window.shinyreact;
const h = React.createElement;
-function OutputCard({ label, title, count }) {
+// --- histogram chart -------------------------------------------------------
+
+const W = 620;
+const H = 380;
+const M = { top: 16, right: 16, bottom: 48, left: 56 };
+const PLOT_W = W - M.left - M.right;
+const PLOT_H = H - M.top - M.bottom;
+
+// Round `max` up to a friendly axis top, using a 1/2/5 × 10^n tick step.
+function yTicks(max) {
+ const raw = Math.max(max, 1) / 4;
+ const mag = Math.pow(10, Math.floor(Math.log10(raw)));
+ const step = [1, 2, 5, 10].map((m) => m * mag).find((s) => s >= raw);
+ const top = Math.ceil(max / step) * step;
+ const ticks = [];
+ for (let v = 0; v <= top; v += step) ticks.push(v);
+ return { top, ticks };
+}
+
+function xTicks(lo, hi) {
+ const step = 10;
+ const ticks = [];
+ for (let v = Math.ceil(lo / step) * step; v <= hi; v += step) ticks.push(v);
+ return ticks;
+}
+
+function Histogram({ data }) {
+ const { breaks, counts } = data;
+ const lo = breaks[0];
+ const hi = breaks[breaks.length - 1];
+ const { top, ticks } = yTicks(Math.max(...counts));
+
+ const x = (v) => M.left + ((v - lo) / (hi - lo)) * PLOT_W;
+ const y = (v) => M.top + PLOT_H - (v / top) * PLOT_H;
+
return h(
- "div",
+ "svg",
{
- style: {
- padding: "1rem",
- background: "#f0f0f0",
- borderRadius: "8px",
- marginBottom: "1rem",
- },
+ viewBox: `0 0 ${W} ${H}`,
+ width: "100%",
+ role: "img",
+ "aria-label": `Histogram of Old Faithful waiting times in ${counts.length} bins`,
},
- h(
- "p",
- {
- style: {
- fontSize: "0.75rem",
- textTransform: "uppercase",
- letterSpacing: "0.05em",
- color: "#888",
- margin: "0 0 0.5rem 0",
+ // y gridlines + labels
+ ticks.map((t) =>
+ h(
+ "g",
+ { key: `y${t}` },
+ h("line", {
+ x1: M.left,
+ x2: M.left + PLOT_W,
+ y1: y(t),
+ y2: y(t),
+ stroke: "#e5e5e5",
+ }),
+ h(
+ "text",
+ {
+ x: M.left - 10,
+ y: y(t),
+ textAnchor: "end",
+ dominantBaseline: "middle",
+ fontSize: 12,
+ fill: "#666",
+ },
+ t,
+ ),
+ ),
+ ),
+ // bars
+ counts.map((count, i) => {
+ const x0 = x(breaks[i]);
+ const x1 = x(breaks[i + 1]);
+ return h("rect", {
+ key: i,
+ x: x0,
+ width: Math.max(x1 - x0 - 1, 1),
+ y: y(count),
+ height: M.top + PLOT_H - y(count),
+ fill: "#447099",
+ });
+ }),
+ // x axis
+ h("line", {
+ x1: M.left,
+ x2: M.left + PLOT_W,
+ y1: M.top + PLOT_H,
+ y2: M.top + PLOT_H,
+ stroke: "#888",
+ }),
+ xTicks(lo, hi).map((t) =>
+ h(
+ "text",
+ {
+ key: `x${t}`,
+ x: x(t),
+ y: M.top + PLOT_H + 20,
+ textAnchor: "middle",
+ fontSize: 12,
+ fill: "#666",
},
- },
- label,
+ t,
+ ),
),
h(
- "p",
- { style: { fontSize: "1.25rem", margin: 0 } },
- title != null ? title : " ",
+ "text",
+ {
+ x: M.left + PLOT_W / 2,
+ y: H - 8,
+ textAnchor: "middle",
+ fontSize: 13,
+ fill: "#333",
+ },
+ "Waiting time to next eruption (minutes)",
),
h(
- "p",
- { style: { color: "#666", margin: "0.5rem 0 0 0" } },
- count != null ? `Count: ${count}` : " ",
+ "text",
+ {
+ transform: `translate(16 ${M.top + PLOT_H / 2}) rotate(-90)`,
+ textAnchor: "middle",
+ fontSize: 13,
+ fill: "#333",
+ },
+ "Frequency",
),
);
}
+// --- app -------------------------------------------------------------------
+
function App() {
const initialized = useShinyInitialized();
- const [name, setName] = useShinyInput("name", "");
- const [clickCount, setClickCount] = useShinyInput("click_count", 0, {
- debounceMs: 0,
- priority: "event",
- });
- const serverTitle = useShinyOutputValue("txtout_title", null);
- const serverCount = useShinyOutputValue("txtout_count", null);
+ const [bins, setBins] = useShinyInput("bins", 30);
+ const data = useShinyOutputValue("dist_data", null);
+ const caption = useShinyOutputValue("dist_caption", null);
+ const status = useShinyOutputStatus("dist_data");
if (!initialized) return null;
- const clientTitle = `Hello, ${name || "World"}!`;
-
return h(
- "div",
- {
- style: { fontFamily: "system-ui", maxWidth: "400px", margin: "2rem auto" },
- },
- h("h1", null, "Hello, ui.tsx!"),
- h("label", { htmlFor: "name-input" }, "Your name:"),
- h("input", {
- id: "name-input",
- type: "text",
- value: name,
- onChange: (e) => setName(e.target.value),
- style: {
- display: "block",
- width: "100%",
- padding: "0.5rem",
- marginTop: "0.25rem",
- marginBottom: "1rem",
- fontSize: "1rem",
- border: "1px solid #ccc",
- borderRadius: "4px",
- },
- }),
+ "main",
+ { className: "layout" },
h(
- "button",
- {
- onClick: () => setClickCount(clickCount + 1),
- style: {
- padding: "0.5rem 1rem",
- fontSize: "1rem",
- cursor: "pointer",
- marginBottom: "1rem",
- },
- },
- `Click me (${clickCount})`,
+ "aside",
+ { className: "sidebar" },
+ h("label", { htmlFor: "bins" }, "Number of bins:"),
+ h("input", {
+ id: "bins",
+ type: "range",
+ min: 1,
+ max: 50,
+ value: bins,
+ onChange: (e) => setBins(Number(e.target.value)),
+ }),
+ h("output", { htmlFor: "bins", className: "bins-value" }, bins),
+ ),
+ h(
+ "section",
+ { className: "panel" },
+ h("h1", null, "Hello Shiny!"),
+ h("p", { className: "caption" }, caption ?? " "),
+ // Keep the chart mounted while the server recomputes — only show the
+ // placeholder before the first value has ever arrived.
+ data
+ ? h(
+ "div",
+ { className: status === "recalculating" ? "recalculating" : "" },
+ h(Histogram, { data }),
+ )
+ : h("div", { className: "placeholder" }, "Loading…"),
),
- h("hr", {
- style: {
- border: "none",
- borderTop: "1px solid #ddd",
- margin: "1.5rem 0",
- },
- }),
- h(OutputCard, { label: "Client", title: clientTitle, count: clickCount }),
- h(OutputCard, { label: "Server", title: serverTitle, count: serverCount }),
);
}
-const root = ReactDOM.createRoot(document.getElementById("root"));
+// No mount div in index.html — create the container and append it to .
+// The script is deferred, so document.body is parsed by the time this runs.
+const root = ReactDOM.createRoot(
+ document.body.appendChild(document.createElement("div")),
+);
root.render(h(App));
diff --git a/examples/01-hello/www/index.html b/examples/01-hello/www/index.html
index 7a54efb1..880e7633 100644
--- a/examples/01-hello/www/index.html
+++ b/examples/01-hello/www/index.html
@@ -1,3 +1,2 @@
-
diff --git a/examples/01-hello/www/main.css b/examples/01-hello/www/main.css
index 4b6d6276..7a6a0aa4 100644
--- a/examples/01-hello/www/main.css
+++ b/examples/01-hello/www/main.css
@@ -2,4 +2,69 @@ body {
margin: 0;
padding: 0;
background: #fff;
+ font-family: system-ui, sans-serif;
+ color: #1a1a1a;
+}
+
+.layout {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1.5rem;
+ max-width: 60rem;
+ margin: 2rem auto;
+ padding: 0 1.5rem;
+}
+
+.sidebar {
+ flex: 1 1 12rem;
+ align-self: start;
+ padding: 1rem;
+ background: #f5f5f5;
+ border-radius: 8px;
+}
+
+.sidebar label {
+ display: block;
+ margin-bottom: 0.5rem;
+ font-size: 0.9rem;
+}
+
+.sidebar input[type="range"] {
+ width: 100%;
+}
+
+.bins-value {
+ display: block;
+ margin-top: 0.25rem;
+ font-variant-numeric: tabular-nums;
+ color: #666;
+}
+
+.panel {
+ flex: 3 1 24rem;
+}
+
+.panel h1 {
+ margin: 0;
+ font-size: 1.5rem;
+}
+
+.caption {
+ margin: 0.25rem 0 1rem;
+ color: #666;
+ font-size: 0.9rem;
+}
+
+.placeholder {
+ display: grid;
+ place-items: center;
+ height: 20rem;
+ border-radius: 8px;
+ background: #f5f5f5;
+ color: #888;
+}
+
+.recalculating {
+ opacity: 0.6;
+ transition: opacity 200ms;
}
diff --git a/examples/02-columns/README.md b/examples/02-columns/README.md
index 78c8c91d..dfcd901f 100644
--- a/examples/02-columns/README.md
+++ b/examples/02-columns/README.md
@@ -26,7 +26,7 @@ examples/02-columns/
└── main.css
```
-Same 4-file shape as [01-hello](../01-hello/). No `package.json`, no bundler.
+Same no-build shape as [01-hello](../01-hello/). No `package.json`, no bundler.
## Bridge primitives used
diff --git a/examples/README.md b/examples/README.md
index e236020c..4c9e8e4d 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -10,7 +10,7 @@ showing the same app on the R package.
| Example | Description |
|---------|-------------|
-| [01-hello](01-hello/) | Smallest `ui.tsx` app — server with reactive logic only, plus a static React client (no JSX, no bundler). Side-by-side comparison of client-only state vs. server-routed state to highlight websocket latency. Includes both `app.py` and `app.R` servers over the same `www/` client |
+| [01-hello](01-hello/) | Shiny's `01_hello` Old Faithful app rebuilt `ui.tsx`-first (no JSX, no bundler). The server returns histogram `{breaks, counts}` as JSON; the client draws the bars as SVG. Includes `app.py`, `app-core.py`, and `app.R` servers over the same `www/` client |
| [02-columns](02-columns/) | Drag-between-columns demo, no build step. Server owns data only (one `move_item` event input), client owns UI (~20 lines of server logic) |
| [03-columns-shadcn](03-columns-shadcn/) | Same drag-between-columns demo as 02, rendered with real shadcn/ui `Card` + `Button` and lucide-react icons. Vite lib-mode IIFE build with React externalized to `window.shinyreact` |
| [04-shadcn](04-shadcn/) | shadcn/ui + Tailwind v4. Side-by-side matplotlib (`@render.plot` + `ImageOutput`) vs. Plotly (data-only via `@reactive_output`, client renders); Plotly hover/click/select events round-trip through `useShinyInput` |