From e5bc55319ba84ac88ad09d584a9418e9c68d1547 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Wed, 19 Aug 2026 16:44:05 -0400 Subject: [PATCH 1/2] docs(01-hello): rebuild example as Shiny's Old Faithful 01_hello MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the name/counter latency demo with Shiny's canonical 01_hello app — a bins slider over the Old Faithful waiting times — rebuilt ui.tsx-first. The server never renders a picture. One reactive_output returns histogram {breaks, counts} as plain JSON plus a caption; www/app.js reads it with useShinyOutputValue and draws the bars as SVG rects. That contrast with traditional Shiny's renderPlot({ hist(...) }) is the point of the example. - faithful.py: waiting times + a stdlib-only binner shared by app.py and app-core.py, so the Python side stays dependency-free (no numpy/matplotlib) - faithful.csv: base R's `faithful` exported for the Python servers; app.R uses the built-in dataset and hist(..., plot = FALSE) - app.R wraps the histogram vectors in I() so bins = 1 serializes as a JSON array rather than a scalar, and returns NULL until the client's first bins message arrives (Python raises a silent exception instead) - The chart stays mounted while the server recomputes and only dims via useShinyOutputStatus, per the repo's flicker guidance Verified in a browser against app.py, app-core.py, and app.R: R and Python produce identical counts at bins 1/2/7/30/50, and the console is clean. --- examples/01-hello/README.md | 60 ++++++-- examples/01-hello/app-core.py | 17 +- examples/01-hello/app.R | 38 ++++- examples/01-hello/app.py | 19 +-- examples/01-hello/faithful.csv | 273 +++++++++++++++++++++++++++++++++ examples/01-hello/faithful.py | 27 ++++ examples/01-hello/www/app.js | 237 ++++++++++++++++++---------- examples/01-hello/www/main.css | 65 ++++++++ examples/02-columns/README.md | 2 +- examples/README.md | 2 +- 10 files changed, 616 insertions(+), 124 deletions(-) create mode 100644 examples/01-hello/faithful.csv create mode 100644 examples/01-hello/faithful.py diff --git a/examples/01-hello/README.md b/examples/01-hello/README.md index 16bedc73..16946374 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 + ├── 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..8c484bca 100644 --- a/examples/01-hello/www/app.js +++ b/examples/01-hello/www/app.js @@ -1,104 +1,185 @@ -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 }), ); } 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` | From ff6f00593995a03616298edd5bdeb720a879ac51 Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Wed, 19 Aug 2026 16:47:26 -0400 Subject: [PATCH 2/2] refactor(examples): mount 01-hello by appending to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Carries #206's change forward onto the rewritten example. www/index.html is now two lines (stylesheet + deferred script) and app.js creates its own mount container via document.body.appendChild(document.createElement("div")) — safe because the script is deferred, so is parsed when it runs. This prepares 01-hello for the upcoming page_react() page mode, where the server emits no body HTML at all and the client owns its mount point. --- examples/01-hello/README.md | 2 +- examples/01-hello/www/app.js | 6 +++++- examples/01-hello/www/index.html | 1 - 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/01-hello/README.md b/examples/01-hello/README.md index 16946374..840c64ee 100644 --- a/examples/01-hello/README.md +++ b/examples/01-hello/README.md @@ -42,7 +42,7 @@ examples/01-hello/ ├── 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 + ├── 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 ``` diff --git a/examples/01-hello/www/app.js b/examples/01-hello/www/app.js index 8c484bca..b5a85903 100644 --- a/examples/01-hello/www/app.js +++ b/examples/01-hello/www/app.js @@ -183,5 +183,9 @@ function App() { ); } -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 @@ -