├── examples ├── 2-inputs │ ├── .gitignore │ ├── srcts │ │ ├── Card.tsx │ │ ├── main.tsx │ │ ├── TextInputCard.tsx │ │ ├── NumberInputCard.tsx │ │ ├── InputOutputCard.tsx │ │ ├── App.tsx │ │ ├── CheckboxInputCard.tsx │ │ ├── SelectInputCard.tsx │ │ ├── DateInputCard.tsx │ │ ├── SliderInputCard.tsx │ │ ├── ButtonInputCard.tsx │ │ ├── RadioInputCard.tsx │ │ ├── FileInputCard.tsx │ │ └── BatchFormCard.tsx │ ├── tsconfig.json │ ├── r │ │ ├── app.R │ │ └── shinyreact.R │ ├── py │ │ ├── app.py │ │ └── shinyreact.py │ ├── package.json │ └── README.md ├── 3-outputs │ ├── .gitignore │ ├── srcts │ │ ├── Card.tsx │ │ ├── main.tsx │ │ ├── PlotCard.tsx │ │ ├── App.tsx │ │ ├── SliderCard.tsx │ │ ├── InputOutputCard.tsx │ │ ├── DataTableCard.tsx │ │ └── StatisticsCard.tsx │ ├── tsconfig.json │ ├── package.json │ ├── r │ │ ├── app.R │ │ ├── mtcars.csv │ │ └── shinyreact.R │ └── py │ │ ├── mtcars.csv │ │ ├── app.py │ │ └── shinyreact.py ├── 4-messages │ ├── .gitignore │ ├── srcts │ │ ├── main.tsx │ │ └── App.tsx │ ├── tsconfig.json │ ├── r │ │ ├── app.R │ │ └── shinyreact.R │ ├── py │ │ ├── app.py │ │ └── shinyreact.py │ └── package.json ├── 5-shadcn │ ├── .gitignore │ ├── srcts │ │ ├── css.d.ts │ │ ├── lib │ │ │ └── utils.ts │ │ ├── main.tsx │ │ └── components │ │ │ ├── PlotCard.tsx │ │ │ ├── ui │ │ │ ├── separator.tsx │ │ │ ├── input.tsx │ │ │ ├── badge.tsx │ │ │ ├── button.tsx │ │ │ └── card.tsx │ │ │ ├── App.tsx │ │ │ ├── ButtonEventCard.tsx │ │ │ └── TextInputCard.tsx │ ├── components.json │ ├── tsconfig.json │ ├── package.json │ ├── r │ │ ├── app.R │ │ └── shinyreact.R │ ├── py │ │ ├── app.py │ │ └── shinyreact.py │ └── build.ts ├── 6-dashboard │ ├── .gitignore │ ├── srcts │ │ ├── css.d.ts │ │ ├── lib │ │ │ └── utils.ts │ │ ├── components │ │ │ ├── ui │ │ │ │ ├── skeleton.tsx │ │ │ │ ├── separator.tsx │ │ │ │ ├── input.tsx │ │ │ │ ├── badge.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── card.tsx │ │ │ │ └── table.tsx │ │ │ ├── Dashboard.tsx │ │ │ ├── Sidebar.tsx │ │ │ └── MetricsCards.tsx │ │ └── main.tsx │ ├── components.json │ ├── tsconfig.json │ ├── package.json │ ├── r │ │ └── shinyreact.R │ ├── py │ │ ├── app.py │ │ └── shinyreact.py │ └── build.ts ├── 1-hello-world │ ├── .gitignore │ ├── r │ │ ├── app.R │ │ └── shinyreact.R │ ├── srcts │ │ ├── main.tsx │ │ └── HelloWorldComponent.tsx │ ├── py │ │ ├── app.py │ │ └── shinyreact.py │ ├── tsconfig.json │ ├── package.json │ └── README.md └── 7-chat │ ├── .gitignore │ ├── py │ ├── requirements.txt │ ├── shinyreact.py │ └── app.py │ ├── srcts │ ├── lib │ │ └── utils.ts │ ├── main.tsx │ ├── components │ │ ├── ui │ │ │ ├── input.tsx │ │ │ ├── avatar.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── button.tsx │ │ │ └── card.tsx │ │ └── ImagePreview.tsx │ ├── hooks │ │ ├── useDragAndDrop.ts │ │ └── useImageUpload.ts │ └── contexts │ │ └── ThemeContext.tsx │ ├── tsconfig.json │ ├── components.json │ ├── package.json │ ├── r │ ├── shinyreact.R │ └── app.R │ └── build.ts ├── docs ├── 7-chat.jpeg ├── 2-inputs.jpeg ├── 5-shadcn.jpeg ├── 3-outputs.jpeg ├── 4-messages.jpeg ├── 6-dashboard.jpeg └── 1-hello-world.jpeg ├── .gitignore ├── .prettierrc ├── src ├── get-shiny.ts ├── index.ts ├── react-registry.ts ├── utils.ts ├── input-registry.ts └── message-registry.ts ├── scripts ├── clean-all-examples.sh └── build-all-examples.sh ├── tsconfig.json ├── LICENSE ├── package.json ├── .github └── workflows │ └── build-shinylive-links.yml ├── .vscode └── settings.json └── eslint.config.js /examples/2-inputs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | r/www/ 3 | py/www/ 4 | -------------------------------------------------------------------------------- /examples/3-outputs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | r/www/ 3 | py/www/ 4 | -------------------------------------------------------------------------------- /examples/4-messages/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | r/www/ 3 | py/www/ 4 | -------------------------------------------------------------------------------- /examples/5-shadcn/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | r/www/ 3 | py/www/ 4 | -------------------------------------------------------------------------------- /examples/6-dashboard/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | r/www/ 3 | py/www/ 4 | -------------------------------------------------------------------------------- /examples/1-hello-world/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | r/www/ 3 | py/www/ 4 | -------------------------------------------------------------------------------- /examples/7-chat/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | r/www/ 3 | py/www/ 4 | .env 5 | -------------------------------------------------------------------------------- /examples/7-chat/py/requirements.txt: -------------------------------------------------------------------------------- 1 | python-dotenv 2 | shiny 3 | chatlas 4 | -------------------------------------------------------------------------------- /docs/7-chat.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wch/shiny-react/HEAD/docs/7-chat.jpeg -------------------------------------------------------------------------------- /docs/2-inputs.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wch/shiny-react/HEAD/docs/2-inputs.jpeg -------------------------------------------------------------------------------- /docs/5-shadcn.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wch/shiny-react/HEAD/docs/5-shadcn.jpeg -------------------------------------------------------------------------------- /docs/3-outputs.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wch/shiny-react/HEAD/docs/3-outputs.jpeg -------------------------------------------------------------------------------- /docs/4-messages.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wch/shiny-react/HEAD/docs/4-messages.jpeg -------------------------------------------------------------------------------- /docs/6-dashboard.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wch/shiny-react/HEAD/docs/6-dashboard.jpeg -------------------------------------------------------------------------------- /docs/1-hello-world.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wch/shiny-react/HEAD/docs/1-hello-world.jpeg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | __pycache__/ 3 | dist/ 4 | *.py[cod] 5 | .DS_Store 6 | examples/*/package-lock.json 7 | shinylive-pages/ 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-organize-imports"], 3 | "organizeImportsSkipDestructiveCodeActions": true 4 | } 5 | -------------------------------------------------------------------------------- /examples/5-shadcn/srcts/css.d.ts: -------------------------------------------------------------------------------- 1 | // This allows globals.css to be imported in main.tsx 2 | declare module "*.css" { 3 | const content: Record; 4 | export default content; 5 | } 6 | -------------------------------------------------------------------------------- /examples/6-dashboard/srcts/css.d.ts: -------------------------------------------------------------------------------- 1 | // This allows globals.css to be imported in main.tsx 2 | declare module "*.css" { 3 | const content: Record; 4 | export default content; 5 | } 6 | -------------------------------------------------------------------------------- /examples/7-chat/srcts/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /examples/5-shadcn/srcts/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /examples/6-dashboard/srcts/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/get-shiny.ts: -------------------------------------------------------------------------------- 1 | import { type ShinyClassExtended } from "./index"; 2 | 3 | /** 4 | * Get the Shiny object if it is available 5 | */ 6 | export function getShiny(): ShinyClassExtended | undefined { 7 | return window.Shiny; 8 | } 9 | -------------------------------------------------------------------------------- /examples/1-hello-world/r/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | 3 | source("shinyreact.R", local = TRUE) 4 | 5 | server <- function(input, output, session) { 6 | output$txtout <- render_json({ 7 | toupper(input$txtin) 8 | }) 9 | } 10 | 11 | shinyApp(ui = page_react(title = "Hello Shiny React"), server = server) 12 | -------------------------------------------------------------------------------- /examples/2-inputs/srcts/Card.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface CardProps { 4 | title: string; 5 | children: React.ReactNode; 6 | } 7 | 8 | function Card({ title, children }: CardProps) { 9 | return ( 10 |
11 |

{title}

12 | {children} 13 |
14 | ); 15 | } 16 | 17 | export default Card; 18 | -------------------------------------------------------------------------------- /examples/3-outputs/srcts/Card.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface CardProps { 4 | title: string; 5 | children: React.ReactNode; 6 | } 7 | 8 | function Card({ title, children }: CardProps) { 9 | return ( 10 |
11 |

{title}

12 | {children} 13 |
14 | ); 15 | } 16 | 17 | export default Card; 18 | -------------------------------------------------------------------------------- /examples/6-dashboard/srcts/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /examples/3-outputs/srcts/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import App from "./App"; 3 | import "./styles.css"; 4 | 5 | const container = document.getElementById("root"); 6 | if (container) { 7 | const root = createRoot(container); 8 | root.render(); 9 | } else { 10 | console.error("Could not find root element to mount React component."); 11 | } 12 | -------------------------------------------------------------------------------- /examples/4-messages/srcts/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import App from "./App"; 3 | import "./styles.css"; 4 | 5 | const container = document.getElementById("root"); 6 | if (container) { 7 | const root = createRoot(container); 8 | root.render(); 9 | } else { 10 | console.error("Could not find root element to mount React component."); 11 | } 12 | -------------------------------------------------------------------------------- /examples/5-shadcn/srcts/main.tsx: -------------------------------------------------------------------------------- 1 | import { App } from "@/components/App"; 2 | import { createRoot } from "react-dom/client"; 3 | import "./globals.css"; 4 | 5 | const container = document.getElementById("root"); 6 | if (container) { 7 | const root = createRoot(container); 8 | root.render(); 9 | } else { 10 | console.error("Could not find root element to mount React component."); 11 | } 12 | -------------------------------------------------------------------------------- /examples/3-outputs/srcts/PlotCard.tsx: -------------------------------------------------------------------------------- 1 | import { ImageOutput } from "@posit/shiny-react"; 2 | import React from "react"; 3 | import Card from "./Card"; 4 | 5 | function PlotCard() { 6 | return ( 7 | 8 |
9 | 10 |
11 |
12 | ); 13 | } 14 | 15 | export default PlotCard; 16 | -------------------------------------------------------------------------------- /examples/6-dashboard/srcts/main.tsx: -------------------------------------------------------------------------------- 1 | import { Dashboard } from "@/components/Dashboard"; 2 | import { createRoot } from "react-dom/client"; 3 | import "./globals.css"; 4 | 5 | const container = document.getElementById("root"); 6 | if (container) { 7 | const root = createRoot(container); 8 | root.render(); 9 | } else { 10 | console.error("Could not find root element to mount React component."); 11 | } 12 | -------------------------------------------------------------------------------- /examples/1-hello-world/srcts/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import HelloWorldComponent from "./HelloWorldComponent"; 3 | import "./styles.css"; 4 | 5 | const container = document.getElementById("root"); 6 | if (container) { 7 | const root = createRoot(container); 8 | root.render(); 9 | } else { 10 | console.error("Could not find root element to mount React component."); 11 | } 12 | -------------------------------------------------------------------------------- /examples/1-hello-world/py/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, Inputs, Outputs, Session 2 | from shinyreact import page_react, render_json 3 | from pathlib import Path 4 | 5 | 6 | def server(input: Inputs, output: Outputs, session: Session): 7 | @render_json 8 | def txtout(): 9 | return input.txtin().upper() 10 | 11 | 12 | app = App( 13 | page_react(title="Hello Shiny React"), 14 | server, 15 | static_assets=str(Path(__file__).parent / "www"), 16 | ) 17 | -------------------------------------------------------------------------------- /examples/2-inputs/srcts/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import App from "./App"; 4 | import "./styles.css"; 5 | 6 | const container = document.getElementById("root"); 7 | if (container) { 8 | const root = createRoot(container); 9 | root.render( 10 | 11 | 12 | , 13 | ); 14 | } else { 15 | console.error("Could not find root element to mount React component."); 16 | } 17 | -------------------------------------------------------------------------------- /examples/7-chat/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "es2022", 5 | "noEmit": true, 6 | "moduleResolution": "node", 7 | "lib": ["es2022", "DOM", "DOM.Iterable"], 8 | "jsx": "react-jsx", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "isolatedModules": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "@/*": ["srcts/*"] 16 | } 17 | }, 18 | "include": ["srcts/**/*.ts", "srcts/**/*.tsx"] 19 | } -------------------------------------------------------------------------------- /examples/2-inputs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "es2022", 5 | "noEmit": true, 6 | "moduleResolution": "node", 7 | "lib": ["es2022", "DOM", "DOM.Iterable"], 8 | "jsx": "react-jsx", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "isolatedModules": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "@/*": ["./srcts/*"] 16 | } 17 | }, 18 | "include": ["srcts/**/*.ts", "srcts/**/*.tsx"] 19 | } 20 | -------------------------------------------------------------------------------- /examples/3-outputs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "es2022", 5 | "noEmit": true, 6 | "moduleResolution": "node", 7 | "lib": ["es2022", "DOM", "DOM.Iterable"], 8 | "jsx": "react-jsx", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "isolatedModules": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "@/*": ["./srcts/*"] 16 | } 17 | }, 18 | "include": ["srcts/**/*.ts", "srcts/**/*.tsx"] 19 | } 20 | -------------------------------------------------------------------------------- /examples/4-messages/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "es2022", 5 | "noEmit": true, 6 | "moduleResolution": "node", 7 | "lib": ["es2022", "DOM", "DOM.Iterable"], 8 | "jsx": "react-jsx", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "isolatedModules": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "@/*": ["./srcts/*"] 16 | } 17 | }, 18 | "include": ["srcts/**/*.ts", "srcts/**/*.tsx"] 19 | } 20 | -------------------------------------------------------------------------------- /examples/1-hello-world/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "es2022", 5 | "noEmit": true, 6 | "moduleResolution": "node", 7 | "lib": ["es2022", "DOM", "DOM.Iterable"], 8 | "jsx": "react-jsx", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "skipLibCheck": true, 12 | "isolatedModules": true, 13 | "baseUrl": ".", 14 | "paths": { 15 | "@/*": ["./srcts/*"] 16 | } 17 | }, 18 | "include": ["srcts/**/*.ts", "srcts/**/*.tsx"] 19 | } 20 | -------------------------------------------------------------------------------- /examples/7-chat/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "srcts/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /examples/6-dashboard/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "srcts/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /examples/7-chat/srcts/main.tsx: -------------------------------------------------------------------------------- 1 | import ChatInterface from "@/components/ChatInterface"; 2 | import { ThemeProvider } from "@/contexts/ThemeContext"; 3 | import "@/globals.css"; 4 | import { createRoot } from "react-dom/client"; 5 | 6 | const container = document.getElementById("root"); 7 | if (container) { 8 | const root = createRoot(container); 9 | root.render( 10 | 11 | 12 | , 13 | ); 14 | } else { 15 | console.error("Could not find root element to mount React component."); 16 | } 17 | -------------------------------------------------------------------------------- /examples/5-shadcn/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /examples/3-outputs/srcts/App.tsx: -------------------------------------------------------------------------------- 1 | import DataTableCard from "./DataTableCard"; 2 | import PlotCard from "./PlotCard"; 3 | import SliderCard from "./SliderCard"; 4 | import StatisticsCard from "./StatisticsCard"; 5 | 6 | function App() { 7 | return ( 8 |
9 |

Shiny React Output Examples

10 |
11 | 12 | 13 | 14 | 15 |
16 |
17 | ); 18 | } 19 | 20 | export default App; 21 | -------------------------------------------------------------------------------- /examples/5-shadcn/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "es2022", 5 | "noEmit": true, 6 | "moduleResolution": "node", 7 | "lib": ["es2022", "DOM", "DOM.Iterable"], 8 | "jsx": "react-jsx", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "noImplicitAny": true, 12 | "skipLibCheck": true, 13 | "isolatedModules": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "@/*": ["./srcts/*"] 17 | } 18 | }, 19 | "include": ["srcts/**/*.ts", "srcts/**/*.tsx", "srcts/**/*.d.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/6-dashboard/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2022", 4 | "module": "es2022", 5 | "noEmit": true, 6 | "moduleResolution": "node", 7 | "lib": ["es2022", "DOM", "DOM.Iterable"], 8 | "jsx": "react-jsx", 9 | "strict": true, 10 | "esModuleInterop": true, 11 | "noImplicitAny": true, 12 | "skipLibCheck": true, 13 | "isolatedModules": true, 14 | "baseUrl": ".", 15 | "paths": { 16 | "@/*": ["./srcts/*"] 17 | } 18 | }, 19 | "include": ["srcts/**/*.ts", "srcts/**/*.tsx", "srcts/**/*.d.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/5-shadcn/srcts/components/PlotCard.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 2 | import { ImageOutput } from "@posit/shiny-react"; 3 | 4 | export function PlotCard() { 5 | return ( 6 | 7 | 8 | Plot Output 9 | 10 | 11 |
12 | 16 |
17 |
18 |
19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Main exports for shiny-react JavaScript library 2 | import { type ShinyClass } from "@posit/shiny/srcts/types/src/shiny"; 3 | import { type ShinyMessageRegistry } from "./message-registry"; 4 | import { type ShinyReactRegistry } from "./react-registry"; 5 | 6 | export { ImageOutput } from "./ImageOutput"; 7 | export { 8 | useShinyInitialized, 9 | useShinyInput, 10 | useShinyMessageHandler, 11 | useShinyOutput, 12 | } from "./use-shiny"; 13 | 14 | export type ShinyClassExtended = ShinyClass & { 15 | reactRegistry: ShinyReactRegistry; 16 | messageRegistry: ShinyMessageRegistry; 17 | }; 18 | 19 | declare global { 20 | interface Window { 21 | Shiny?: ShinyClassExtended; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /scripts/clean-all-examples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get list of example directories 4 | EXAMPLE_DIRS=$(find examples -maxdepth 1 -type d -name "*-*" | sort) 5 | 6 | START_DIR="$(pwd)" 7 | 8 | if [ -z "$EXAMPLE_DIRS" ]; then 9 | echo "No example directories found under ./examples." 10 | echo "Are you running this from the top level of the shiny-react repository?" 11 | echo "Expected to find directories like examples/1-hello-world/" 12 | echo "If not, cd to the project root (so that ./examples/ exists) and re-run: scripts/build-all-examples.sh" 13 | exit 1 14 | fi 15 | 16 | echo "Found example directories: $EXAMPLE_DIRS" 17 | 18 | for dir in $EXAMPLE_DIRS; do 19 | echo "Cleaning $dir..." 20 | cd "$dir" 21 | 22 | npm run clean 23 | 24 | cd "$START_DIR" 25 | done 26 | -------------------------------------------------------------------------------- /examples/3-outputs/srcts/SliderCard.tsx: -------------------------------------------------------------------------------- 1 | import { useShinyInput } from "@posit/shiny-react"; 2 | import React from "react"; 3 | import Card from "./Card"; 4 | 5 | function SliderCard() { 6 | const [rowCount, setRowCount] = useShinyInput("table_rows", 4); 7 | 8 | return ( 9 | 10 |
11 | 12 | setRowCount(parseInt(e.target.value))} 19 | className="slider" 20 | /> 21 |
22 |
23 | ); 24 | } 25 | 26 | export default SliderCard; 27 | -------------------------------------------------------------------------------- /examples/3-outputs/srcts/InputOutputCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Card from "./Card"; 3 | 4 | interface InputOutputCardProps { 5 | title: string; 6 | inputElement: React.ReactNode; 7 | outputValue: React.ReactNode; 8 | } 9 | 10 | function InputOutputCard({ 11 | title, 12 | inputElement, 13 | outputValue, 14 | }: InputOutputCardProps) { 15 | return ( 16 | 17 |
18 |
{inputElement}
19 |
20 |
Server response:
21 |
{outputValue}
22 |
23 |
24 |
25 | ); 26 | } 27 | 28 | export default InputOutputCard; 29 | -------------------------------------------------------------------------------- /examples/2-inputs/srcts/TextInputCard.tsx: -------------------------------------------------------------------------------- 1 | import { useShinyInput, useShinyOutput } from "@posit/shiny-react"; 2 | import React from "react"; 3 | import InputOutputCard from "./InputOutputCard"; 4 | 5 | function TextInputCard() { 6 | const [txtin, setTxtin] = useShinyInput("txtin", "Hello, world!"); 7 | const [txtout, _] = useShinyOutput("txtout", undefined); 8 | 9 | const handleInputChange = (event: React.ChangeEvent) => { 10 | setTxtin(event.target.value); 11 | }; 12 | 13 | return ( 14 | 23 | } 24 | outputValue={txtout} 25 | /> 26 | ); 27 | } 28 | 29 | export default TextInputCard; 30 | -------------------------------------------------------------------------------- /examples/5-shadcn/srcts/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 2 | import * as React from "react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Separator = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >( 10 | ( 11 | { className, orientation = "horizontal", decorative = true, ...props }, 12 | ref, 13 | ) => ( 14 | 25 | ), 26 | ); 27 | Separator.displayName = SeparatorPrimitive.Root.displayName; 28 | 29 | export { Separator }; 30 | -------------------------------------------------------------------------------- /examples/6-dashboard/srcts/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as SeparatorPrimitive from "@radix-ui/react-separator"; 2 | import * as React from "react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Separator = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >( 10 | ( 11 | { className, orientation = "horizontal", decorative = true, ...props }, 12 | ref, 13 | ) => ( 14 | 25 | ), 26 | ); 27 | Separator.displayName = SeparatorPrimitive.Root.displayName; 28 | 29 | export { Separator }; 30 | -------------------------------------------------------------------------------- /examples/2-inputs/srcts/NumberInputCard.tsx: -------------------------------------------------------------------------------- 1 | import { useShinyInput, useShinyOutput } from "@posit/shiny-react"; 2 | import React from "react"; 3 | import InputOutputCard from "./InputOutputCard"; 4 | 5 | function NumberInputCard() { 6 | const [numberIn, setNumberIn] = useShinyInput("numberin", 42); 7 | const [numberOut, _] = useShinyOutput("numberout", undefined); 8 | 9 | const handleInputChange = (event: React.ChangeEvent) => { 10 | setNumberIn(Number(event.target.value)); 11 | }; 12 | 13 | return ( 14 | 25 | } 26 | outputValue={numberOut} 27 | /> 28 | ); 29 | } 30 | 31 | export default NumberInputCard; 32 | -------------------------------------------------------------------------------- /examples/5-shadcn/srcts/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | }, 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /examples/6-dashboard/srcts/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | }, 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "rootDir": "src", 5 | "target": "es2022", 6 | "module": "es2022", 7 | "moduleResolution": "node", 8 | "lib": ["es2022", "DOM", "DOM.Iterable"], 9 | "declaration": true, 10 | "declarationMap": true, 11 | "sourceMap": true, 12 | "inlineSources": true, 13 | "checkJs": false, 14 | "skipLibCheck": true, 15 | "isolatedModules": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitAny": true, 19 | "noImplicitThis": true, 20 | "allowSyntheticDefaultImports": true, 21 | "experimentalDecorators": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "noImplicitOverride": true, 24 | "esModuleInterop": true, 25 | "strict": true, 26 | "jsx": "react-jsx" 27 | }, 28 | "include": ["src/**/*.ts", "src/**/*.tsx", "eslint.config.mjs"], 29 | "exclude": ["node_modules", "dist"] 30 | } 31 | -------------------------------------------------------------------------------- /examples/7-chat/srcts/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | }, 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /examples/2-inputs/srcts/InputOutputCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Card from "./Card"; 3 | 4 | interface InputOutputCardProps { 5 | title: string; 6 | inputElement: React.ReactNode; 7 | outputValue: React.ReactNode; 8 | layout?: "horizontal" | "vertical"; 9 | } 10 | 11 | function InputOutputCard({ 12 | title, 13 | inputElement, 14 | outputValue, 15 | layout = "horizontal", 16 | }: InputOutputCardProps) { 17 | const containerClass = 18 | layout === "vertical" 19 | ? "input-output-container-vertical" 20 | : "input-output-container"; 21 | 22 | return ( 23 | 24 |
25 |
{inputElement}
26 |
27 |
Server response:
28 |
{outputValue}
29 |
30 |
31 |
32 | ); 33 | } 34 | 35 | export default InputOutputCard; 36 | -------------------------------------------------------------------------------- /examples/4-messages/r/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | 3 | source("shinyreact.R", local = TRUE) 4 | 5 | 6 | server <- function(input, output, session) { 7 | output$txtout <- render_json({ 8 | toupper(input$txtin) 9 | }) 10 | 11 | # Simulate log events 12 | log_messages <- list( 13 | list(text = "User logged in", category = "info"), 14 | list(text = "File saved successfully", category = "success"), 15 | list(text = "Low disk space warning", category = "warning"), 16 | list(text = "Backup completed", category = "success"), 17 | list(text = "Processing data...", category = "info"), 18 | list(text = "Cache cleared", category = "info") 19 | ) 20 | 21 | # Timer that triggers every 2 seconds 22 | observe({ 23 | invalidateLater(2000) 24 | 25 | # Send a random log message 26 | log_event <- log_messages[[sample(seq_along(log_messages), 1)]] 27 | post_message(session, "logEvent", log_event) 28 | }) 29 | } 30 | 31 | shinyApp( 32 | ui = page_react(title = "Server-to-client messages - Shiny React"), 33 | server = server 34 | ) 35 | -------------------------------------------------------------------------------- /examples/2-inputs/srcts/App.tsx: -------------------------------------------------------------------------------- 1 | import BatchFormCard from "./BatchFormCard"; 2 | import ButtonInputCard from "./ButtonInputCard"; 3 | import CheckboxInputCard from "./CheckboxInputCard"; 4 | import DateInputCard from "./DateInputCard"; 5 | import FileInputCard from "./FileInputCard"; 6 | import NumberInputCard from "./NumberInputCard"; 7 | import RadioInputCard from "./RadioInputCard"; 8 | import SelectInputCard from "./SelectInputCard"; 9 | import SliderInputCard from "./SliderInputCard"; 10 | import TextInputCard from "./TextInputCard"; 11 | 12 | function App() { 13 | return ( 14 |
15 |

Shiny React Input Examples

16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 | ); 30 | } 31 | 32 | export default App; 33 | -------------------------------------------------------------------------------- /examples/5-shadcn/srcts/components/App.tsx: -------------------------------------------------------------------------------- 1 | import { Separator } from "@/components/ui/separator"; 2 | import React from "react"; 3 | import { ButtonEventCard } from "./ButtonEventCard"; 4 | import { PlotCard } from "./PlotCard"; 5 | import { TextInputCard } from "./TextInputCard"; 6 | 7 | export function App() { 8 | return ( 9 |
10 |
11 |
12 |

13 | Shiny + React + shadcn/ui 14 |

15 |

16 | Demonstrating shadcn/ui components with various shiny-react output 17 | types 18 |

19 |
20 | 21 | 22 | 23 |
24 | 25 | 26 |
27 | 28 |
29 | 30 |
31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /examples/4-messages/py/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, Inputs, Outputs, Session, reactive 2 | from shinyreact import page_react, post_message 3 | from pathlib import Path 4 | import random 5 | 6 | 7 | def server(input: Inputs, output: Outputs, session: Session): 8 | # Simulate log events 9 | log_messages = [ 10 | {"text": "User logged in", "category": "info"}, 11 | {"text": "File saved successfully", "category": "success"}, 12 | {"text": "Low disk space warning", "category": "warning"}, 13 | {"text": "Backup completed", "category": "success"}, 14 | {"text": "Processing data...", "category": "info"}, 15 | {"text": "Cache cleared", "category": "info"}, 16 | ] 17 | 18 | @reactive.effect 19 | async def _(): 20 | # Timer that triggers every 2 seconds 21 | reactive.invalidate_later(2) 22 | log_event = random.choice(log_messages) 23 | await post_message(session, "logEvent", log_event) 24 | 25 | 26 | app = App( 27 | page_react(title="Server-to-client messages - Shiny React"), 28 | server, 29 | static_assets=str(Path(__file__).parent / "www"), 30 | ) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Posit Software, PBC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/1-hello-world/srcts/HelloWorldComponent.tsx: -------------------------------------------------------------------------------- 1 | import { useShinyInput, useShinyOutput } from "@posit/shiny-react"; 2 | import React from "react"; 3 | 4 | function HelloWorldComponent() { 5 | const [txtin, setTxtin] = useShinyInput("txtin", "Hello, world!"); 6 | 7 | const [txtout, _] = useShinyOutput("txtout", undefined); 8 | 9 | const handleInputChange = (event: React.ChangeEvent) => { 10 | setTxtin(event.target.value); 11 | }; 12 | 13 | return ( 14 |
15 |

Hello Shiny React!

16 |
17 | 18 | 24 |
25 |
26 |
27 | 28 |
{txtout}
29 |
30 |
31 | ); 32 | } 33 | 34 | export default HelloWorldComponent; 35 | -------------------------------------------------------------------------------- /examples/2-inputs/srcts/CheckboxInputCard.tsx: -------------------------------------------------------------------------------- 1 | import { useShinyInput, useShinyOutput } from "@posit/shiny-react"; 2 | import React from "react"; 3 | import InputOutputCard from "./InputOutputCard"; 4 | 5 | function CheckboxInputCard() { 6 | const [checkboxIn, setCheckboxIn] = useShinyInput( 7 | "checkboxin", 8 | false, 9 | { debounceMs: 0 }, 10 | ); 11 | const [checkboxOut, _] = useShinyOutput("checkboxout", undefined); 12 | 13 | const handleInputChange = (event: React.ChangeEvent) => { 14 | setCheckboxIn(event.target.checked); 15 | }; 16 | 17 | return ( 18 | 22 | 32 |
33 | } 34 | outputValue={checkboxOut} 35 | /> 36 | ); 37 | } 38 | 39 | export default CheckboxInputCard; 40 | -------------------------------------------------------------------------------- /examples/6-dashboard/srcts/components/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { Charts } from "@/components/Charts"; 2 | import { DataTable } from "@/components/DataTable"; 3 | import { MetricsCards } from "@/components/MetricsCards"; 4 | import { Sidebar } from "@/components/Sidebar"; 5 | import { Separator } from "@/components/ui/separator"; 6 | import React from "react"; 7 | 8 | export function Dashboard() { 9 | return ( 10 |
11 |
12 | 13 |
14 |
15 |

Dashboard

16 |

17 | Welcome to your analytics dashboard. Monitor your key metrics and 18 | performance. 19 |

20 |
21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 |
30 |
31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /examples/2-inputs/srcts/SelectInputCard.tsx: -------------------------------------------------------------------------------- 1 | import { useShinyInput, useShinyOutput } from "@posit/shiny-react"; 2 | import React from "react"; 3 | import InputOutputCard from "./InputOutputCard"; 4 | 5 | function SelectInputCard() { 6 | const [selectIn, setSelectIn] = useShinyInput("selectin", "apple", { 7 | debounceMs: 0, 8 | }); 9 | const [selectOut, _] = useShinyOutput("selectout", undefined); 10 | 11 | const handleInputChange = (event: React.ChangeEvent) => { 12 | setSelectIn(event.target.value); 13 | }; 14 | 15 | return ( 16 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | } 31 | outputValue={selectOut} 32 | /> 33 | ); 34 | } 35 | 36 | export default SelectInputCard; 37 | -------------------------------------------------------------------------------- /examples/2-inputs/srcts/DateInputCard.tsx: -------------------------------------------------------------------------------- 1 | import { useShinyInput, useShinyOutput } from "@posit/shiny-react"; 2 | import React from "react"; 3 | import InputOutputCard from "./InputOutputCard"; 4 | 5 | function DateInputCard() { 6 | // Get today's date as default in YYYY-MM-DD format 7 | const today = new Date().toISOString().split("T")[0]; 8 | 9 | const [dateIn, setDateIn] = useShinyInput("datein", today, { 10 | debounceMs: 0, 11 | }); 12 | const [dateOut, _] = useShinyOutput("dateout", undefined); 13 | 14 | const handleInputChange = (event: React.ChangeEvent) => { 15 | setDateIn(event.target.value); 16 | }; 17 | 18 | return ( 19 | 23 | 24 | 30 |
Selected date: {dateIn}
31 | 32 | } 33 | outputValue={dateOut} 34 | /> 35 | ); 36 | } 37 | 38 | export default DateInputCard; 39 | -------------------------------------------------------------------------------- /examples/5-shadcn/srcts/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from "class-variance-authority"; 2 | import * as React from "react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | }, 24 | ); 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ); 34 | } 35 | 36 | export { Badge, badgeVariants }; 37 | -------------------------------------------------------------------------------- /src/react-registry.ts: -------------------------------------------------------------------------------- 1 | import { getShiny } from "./get-shiny"; 2 | import { InputRegistry } from "./input-registry"; 3 | import { OutputRegistry } from "./output-registry"; 4 | 5 | export interface ShinyReactRegistry { 6 | inputs: InputRegistry; 7 | outputs: OutputRegistry; 8 | } 9 | 10 | let reactRegistry: ShinyReactRegistry | undefined = undefined; 11 | /** 12 | * Initialize the global react registry and make it available on window.Shiny 13 | * This function should be called after Shiny is initialized 14 | */ 15 | export function initializeReactRegistry(): void { 16 | // Create registries that can work with or without Shiny 17 | reactRegistry = { 18 | inputs: new InputRegistry(), 19 | outputs: new OutputRegistry(), 20 | }; 21 | 22 | const shiny = getShiny(); 23 | if (!shiny) { 24 | return; 25 | } 26 | shiny.reactRegistry = reactRegistry; 27 | } 28 | 29 | /** 30 | * Get the react registry, whether it's attached to window.Shiny or standalone 31 | */ 32 | export function getReactRegistry(): ShinyReactRegistry { 33 | const shiny = getShiny(); 34 | if (!shiny) { 35 | if (!reactRegistry) { 36 | throw new Error("React registry not initialized"); 37 | } 38 | return reactRegistry; 39 | } 40 | 41 | return shiny.reactRegistry; 42 | } 43 | 44 | export { reactRegistry }; 45 | -------------------------------------------------------------------------------- /examples/6-dashboard/srcts/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from "class-variance-authority"; 2 | import * as React from "react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | }, 24 | ); 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ); 34 | } 35 | 36 | export { Badge, badgeVariants }; 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@posit/shiny-react", 3 | "version": "0.0.16", 4 | "description": "React bindings for Shiny applications (R and Python)", 5 | "author": "Winston Chang", 6 | "license": "MIT", 7 | "type": "module", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/wch/shiny-react.git" 11 | }, 12 | "types": "dist/index.d.ts", 13 | "module": "dist/index.js", 14 | "exports": { 15 | ".": { 16 | "types": "./dist/index.d.ts", 17 | "module": "./dist/index.js" 18 | } 19 | }, 20 | "files": [ 21 | "dist/" 22 | ], 23 | "scripts": { 24 | "build": "tsc", 25 | "watch": "tsc --watch --preserveWatchOutput", 26 | "clean": "rm -rf dist/" 27 | }, 28 | "peerDependencies": { 29 | "react": "^18.2.0 || ^19.0.0", 30 | "react-dom": "^18.2.0 || ^19.0.0" 31 | }, 32 | "devDependencies": { 33 | "@posit/shiny": "^1.11.1", 34 | "@types/node": "^24.3.0", 35 | "@types/react-dom": "^19.1.7", 36 | "@typescript-eslint/eslint-plugin": "^8.25.0", 37 | "@typescript-eslint/parser": "^8.25.0", 38 | "eslint": "^9.21.0", 39 | "eslint-plugin-react": "^7.37.5", 40 | "eslint-plugin-react-hooks": "^5.2.0", 41 | "prettier": "^3.6.2", 42 | "prettier-plugin-organize-imports": "^4.2.0", 43 | "typescript": "^5.9.2", 44 | "typescript-eslint": "^8.41.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/2-inputs/srcts/SliderInputCard.tsx: -------------------------------------------------------------------------------- 1 | import { useShinyInput, useShinyOutput } from "@posit/shiny-react"; 2 | import React from "react"; 3 | import InputOutputCard from "./InputOutputCard"; 4 | 5 | function SliderInputCard() { 6 | // Note that debounce has been set to 0, so that the value is sent immediately. 7 | const [sliderIn, setSliderIn] = useShinyInput("sliderin", 50, { 8 | debounceMs: 0, 9 | }); 10 | const [sliderOut, _] = useShinyOutput("sliderout", undefined); 11 | 12 | const handleInputChange = (event: React.ChangeEvent) => { 13 | setSliderIn(Number(event.target.value)); 14 | }; 15 | 16 | return ( 17 | 21 | 22 | 30 |
Current value: {sliderIn}
31 |
32 | Note: Debounce is set to 0ms for immediate updates 33 |
34 |
35 | } 36 | outputValue={sliderOut} 37 | /> 38 | ); 39 | } 40 | 41 | export default SliderInputCard; 42 | -------------------------------------------------------------------------------- /examples/2-inputs/r/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | 3 | source("shinyreact.R", local = TRUE) 4 | 5 | server <- function(input, output, session) { 6 | output$txtout <- render_json({ 7 | toupper(input$txtin) 8 | }) 9 | 10 | output$numberout <- render_json({ 11 | input$numberin 12 | }) 13 | 14 | output$checkboxout <- render_json({ 15 | as.character(input$checkboxin) 16 | }) 17 | 18 | output$radioout <- render_json({ 19 | input$radioin 20 | }) 21 | 22 | output$selectout <- render_json({ 23 | input$selectin 24 | }) 25 | 26 | output$sliderout <- render_json({ 27 | input$sliderin 28 | }) 29 | 30 | output$dateout <- render_json({ 31 | input$datein 32 | }) 33 | 34 | num_button_clicks <- 0 35 | output$buttonout <- render_json({ 36 | # Take a reactive dependency on the button, and ignore starting null value 37 | if (is.null(input$buttonin)) { 38 | return() 39 | } 40 | num_button_clicks <<- num_button_clicks + 1 41 | num_button_clicks 42 | }) 43 | 44 | output$fileout <- render_json({ 45 | input$filein 46 | }) 47 | 48 | output$batchout <- render_json({ 49 | data <- input$batchdata 50 | if (is.null(data)) { 51 | return("No data submitted yet.") 52 | } 53 | data$receivedAt <- as.character(Sys.time()) 54 | data 55 | }) 56 | } 57 | 58 | shinyApp(ui = page_react(title = "Inputs - Shiny React"), server = server) 59 | -------------------------------------------------------------------------------- /examples/5-shadcn/srcts/components/ButtonEventCard.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 3 | import { useShinyInput, useShinyOutput } from "@posit/shiny-react"; 4 | import React from "react"; 5 | 6 | export function ButtonEventCard() { 7 | const [buttonTrigger, setButtonTrigger] = useShinyInput<{}>( 8 | "button_trigger", 9 | {}, 10 | { priority: "event" }, 11 | ); 12 | const [buttonResponse, _] = useShinyOutput("button_response", ""); 13 | 14 | const handleClick = () => { 15 | setButtonTrigger({}); 16 | }; 17 | 18 | return ( 19 | 20 | 21 | Button Events 22 | 23 | 24 |
25 |

26 | Click to trigger server event: 27 |

28 | 31 |
32 |
33 |

Server response:

34 |
35 |
36 |               {buttonResponse || "Click button to see response"}
37 |             
38 |
39 |
40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /examples/4-messages/srcts/App.tsx: -------------------------------------------------------------------------------- 1 | import { useShinyMessageHandler } from "@posit/shiny-react"; 2 | import React, { useState } from "react"; 3 | 4 | interface ToastMessage { 5 | id: number; 6 | message: string; 7 | type: string; 8 | } 9 | 10 | function App() { 11 | const [toasts, setToasts] = useState([]); 12 | 13 | // Handle log events from the server using useShinyMessage hook 14 | useShinyMessageHandler( 15 | "logEvent", 16 | (msg: { text: string; category: string }) => { 17 | console.log("Received log event message:", msg); 18 | const newToast: ToastMessage = { 19 | id: Date.now(), 20 | message: msg.text, 21 | type: msg.category, 22 | }; 23 | console.log(newToast); 24 | 25 | setToasts((prev) => [...prev, newToast]); 26 | 27 | // Remove toast after 6 seconds 28 | setTimeout(() => { 29 | setToasts((prev) => prev.filter((toast) => toast.id !== newToast.id)); 30 | }, 6000); 31 | }, 32 | ); 33 | 34 | return ( 35 |
36 |

Event Message Demo

37 |
38 |

Toast messages from server

39 |
40 | {toasts.map((toast) => ( 41 |
42 | {toast.message} 43 |
44 | ))} 45 |
46 |
47 |
48 | ); 49 | } 50 | 51 | export default App; 52 | -------------------------------------------------------------------------------- /examples/5-shadcn/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shiny-react-dashboard", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "Interactive dashboard example using shiny-react and shadcn/ui", 6 | "scripts": { 7 | "build": "concurrently -c auto \"tsc --noEmit\" \"tsx build.ts\"", 8 | "watch": "concurrently -c auto \"tsc --noEmit --watch --preserveWatchOutput\" \"tsx build.ts --watch\"", 9 | "build-prod": "tsc --noEmit && tsx build.ts --production", 10 | "clean": "rm -rf r/www py/www build" 11 | }, 12 | "author": "Winston Chang", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/react": "^19.1.1", 16 | "@types/react-dom": "^19.1.1", 17 | "autoprefixer": "^10.4.16", 18 | "chokidar": "^4.0.3", 19 | "concurrently": "^9.2.1", 20 | "esbuild": "^0.25.9", 21 | "esbuild-plugin-tailwindcss": "^2.1.0", 22 | "prettier": "^3.6.2", 23 | "prettier-plugin-organize-imports": "^4.2.0", 24 | "react": "^19.1.1", 25 | "react-dom": "^19.1.1", 26 | "tailwindcss": "^4.1.12", 27 | "tsx": "^4.20.4", 28 | "typescript": "^5.9.2" 29 | }, 30 | "dependencies": { 31 | "@posit/shiny-react": "file:../..", 32 | "@radix-ui/react-separator": "^1.0.3", 33 | "@radix-ui/react-slot": "^1.0.2", 34 | "class-variance-authority": "^0.7.1", 35 | "clsx": "^2.1.1", 36 | "tailwind-merge": "^3.3.1" 37 | }, 38 | "exampleMetadata": { 39 | "title": "shadcn/ui Components", 40 | "description": "Modern UI components using shadcn/ui design system", 41 | "deployToShinylive": true, 42 | "comment": "" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/5-shadcn/r/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | source("shinyreact.R", local = TRUE) 3 | 4 | # Generate sample data 5 | sample_data <- data.frame( 6 | id = 1:8, 7 | age = c(25, 30, 35, 28, 32, 27, 29, 33), 8 | score = c(85.5, 92.1, 88.3, 88.7, 95.2, 81.9, 87.4, 90.6), 9 | category = c("A", "B", "A", "C", "B", "A", "C", "B") 10 | ) 11 | 12 | 13 | server <- function(input, output, session) { 14 | # Process text input 15 | output$processed_text <- render_json({ 16 | text <- input$user_text %||% "" 17 | reversed_text <- paste(rev(strsplit(text, "")[[1]]), collapse = "") 18 | toupper(reversed_text) 19 | }) 20 | 21 | # Calculate text length 22 | output$text_length <- render_json({ 23 | text <- input$user_text %||% "" 24 | nchar(text) 25 | }) 26 | 27 | output$button_response <- render_json({ 28 | paste("Event received at:", as.character(Sys.time(), digits = 2)) 29 | }) |> 30 | bindEvent(input$button_trigger) # Trigger on button events 31 | 32 | # Table data output 33 | output$table_data <- render_json({ 34 | sample_data 35 | }) 36 | 37 | # Plot output 38 | output$plot1 <- renderPlot({ 39 | plot( 40 | sample_data$age, 41 | sample_data$score, 42 | xlab = "Age", 43 | ylab = "Score", 44 | main = "Age vs Score", 45 | pch = 19, 46 | cex = 1.5 47 | ) 48 | 49 | # Add a trend line 50 | abline(lm(score ~ age, data = sample_data), col = "red", lwd = 2) 51 | 52 | # Add grid 53 | grid() 54 | }) 55 | } 56 | 57 | shinyApp( 58 | ui = page_react(title = "Shiny + shadcn/ui Example"), 59 | server = server 60 | ) 61 | -------------------------------------------------------------------------------- /examples/3-outputs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shiny-react-outputs", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "Outputs example app using shiny-react", 6 | "scripts": { 7 | "build": "concurrently -c auto \"npm run build-r\" \"npm run build-py\" \"tsc --noEmit\"", 8 | "watch": "concurrently -c auto \"npm run watch-r\" \"npm run watch-py\" \"tsc --noEmit --watch --preserveWatchOutput\"", 9 | "build-r": "esbuild srcts/main.tsx --bundle --minify --outfile=r/www/main.js --format=esm --alias:react=react", 10 | "watch-r": "esbuild srcts/main.tsx --bundle --minify --outfile=r/www/main.js --format=esm --alias:react=react --watch", 11 | "build-py": "esbuild srcts/main.tsx --bundle --minify --outfile=py/www/main.js --format=esm --alias:react=react", 12 | "watch-py": "esbuild srcts/main.tsx --bundle --minify --outfile=py/www/main.js --format=esm --alias:react=react --watch", 13 | "clean": "rm -rf r/www py/www" 14 | }, 15 | "author": "Winston Chang", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "@types/react-dom": "^19.1.7", 19 | "concurrently": "^9.0.1", 20 | "esbuild": "^0.25.9", 21 | "prettier": "^3.6.2", 22 | "prettier-plugin-organize-imports": "^4.2.0", 23 | "react": "^19.1.1", 24 | "react-dom": "^19.1.1", 25 | "tsx": "^4.20.4", 26 | "typescript": "^5.9.2" 27 | }, 28 | "dependencies": { 29 | "@posit/shiny-react": "file:../.." 30 | }, 31 | "exampleMetadata": { 32 | "title": "Output Components", 33 | "description": "Data visualization and output rendering examples", 34 | "deployToShinylive": true, 35 | "comment": "" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/build-shinylive-links.yml: -------------------------------------------------------------------------------- 1 | name: Build Shinylive Links 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: "pages" 15 | cancel-in-progress: false 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: "20" 29 | 30 | - name: Setup Python 31 | uses: actions/setup-python@v4 32 | with: 33 | python-version: "3.12" 34 | 35 | - name: Install shinylive 36 | run: | 37 | pip install shinylive 38 | # Verify installation 39 | shinylive --version 40 | 41 | - name: Build all example apps 42 | run: | 43 | ./scripts/build-all-examples.sh 44 | 45 | - name: Generate shinylive redirect pages 46 | run: | 47 | python scripts/generate-shinylive-links.py 48 | 49 | - name: Upload pages artifact 50 | uses: actions/upload-pages-artifact@v3 51 | with: 52 | path: shinylive-pages 53 | 54 | deploy: 55 | if: github.ref == 'refs/heads/main' 56 | needs: build 57 | runs-on: ubuntu-latest 58 | 59 | environment: 60 | name: github-pages 61 | url: ${{ steps.deployment.outputs.page_url }} 62 | 63 | steps: 64 | - name: Deploy to GitHub Pages 65 | id: deployment 66 | uses: actions/deploy-pages@v4 67 | -------------------------------------------------------------------------------- /examples/7-chat/srcts/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | import * as AvatarPrimitive from "@radix-ui/react-avatar"; 2 | import * as React from "react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const Avatar = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, ...props }, ref) => ( 10 | 18 | )); 19 | Avatar.displayName = AvatarPrimitive.Root.displayName; 20 | 21 | const AvatarImage = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef 24 | >(({ className, ...props }, ref) => ( 25 | 30 | )); 31 | AvatarImage.displayName = AvatarPrimitive.Image.displayName; 32 | 33 | const AvatarFallback = React.forwardRef< 34 | React.ElementRef, 35 | React.ComponentPropsWithoutRef 36 | >(({ className, ...props }, ref) => ( 37 | 45 | )); 46 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; 47 | 48 | export { Avatar, AvatarFallback, AvatarImage }; 49 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type definition for a debounced function with dynamic delay capabilities 3 | */ 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | export type DebouncedFunction any> = { 6 | (...args: Parameters): void; 7 | setDelay: (newDelay: number) => void; 8 | getDelay: () => number; 9 | cancel: () => void; 10 | }; 11 | 12 | /** 13 | * Creates a debounced function with dynamic delay that can be changed at runtime. 14 | * 15 | * @param func The function to debounce 16 | * @param delay The initial number of milliseconds to delay 17 | * @returns A debounced function with setDelay, getDelay, and cancel methods attached 18 | */ 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | export function createDebouncedFn any>( 21 | func: T, 22 | delay: number, 23 | ): DebouncedFunction { 24 | let timeout: ReturnType | null = null; 25 | 26 | const debouncedFunction = function (...args: Parameters) { 27 | const later = () => { 28 | timeout = null; 29 | func(...args); 30 | }; 31 | 32 | if (timeout !== null) { 33 | clearTimeout(timeout); 34 | } 35 | 36 | timeout = setTimeout(later, delay); 37 | } as DebouncedFunction; 38 | 39 | debouncedFunction.setDelay = (newDelay: number) => { 40 | delay = newDelay; 41 | }; 42 | 43 | debouncedFunction.getDelay = () => delay; 44 | 45 | debouncedFunction.cancel = () => { 46 | if (timeout !== null) { 47 | clearTimeout(timeout); 48 | timeout = null; 49 | } 50 | }; 51 | 52 | return debouncedFunction; 53 | } 54 | -------------------------------------------------------------------------------- /examples/3-outputs/r/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(jsonlite) 3 | 4 | source("shinyreact.R", local = TRUE) 5 | mtcars <- read.csv("mtcars.csv") 6 | 7 | 8 | server <- function(input, output, session) { 9 | output$table_data <- render_json({ 10 | req(input$table_rows) 11 | # This will be converted to a JSON object in column-major format, as in: 12 | # { 13 | # "mpg": [21, 21, 22.8, ...], 14 | # "cyl": [6, 6, 4, ...], 15 | # "disp": [160, 160, 108, ...], 16 | # ... 17 | # } 18 | mtcars[seq_len(input$table_rows), ] 19 | }) 20 | 21 | output$table_stats <- render_json({ 22 | req(input$table_rows) 23 | mtcars_subset <- mtcars[seq_len(input$table_rows), ] 24 | 25 | # Return some summary statistics 26 | list( 27 | colname = "mpg", 28 | mean = mean(mtcars_subset$mpg), 29 | median = median(mtcars_subset$mpg), 30 | min = min(mtcars_subset$mpg), 31 | max = max(mtcars_subset$mpg) 32 | ) 33 | }) 34 | 35 | output$plot1 <- renderPlot({ 36 | req(input$table_rows) 37 | mtcars_subset <- mtcars[seq_len(input$table_rows), ] 38 | 39 | # Create a scatter plot of mpg vs wt 40 | plot( 41 | mtcars_subset$wt, 42 | mtcars_subset$mpg, 43 | xlab = "Weight (1000 lbs)", 44 | ylab = "Miles per Gallon", 45 | main = paste("MPG vs Weight -", nrow(mtcars_subset), "cars"), 46 | col = "steelblue", 47 | pch = 19, 48 | cex = 1.2 49 | ) 50 | 51 | # Add a trend line 52 | abline(lm(mpg ~ wt, data = mtcars_subset), col = "red", lwd = 2) 53 | }) 54 | } 55 | 56 | shinyApp(ui = page_react(title = "Outputs - Shiny React"), server = server) 57 | -------------------------------------------------------------------------------- /examples/4-messages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shiny-react-messages", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "Messages example app using shiny-react", 6 | "scripts": { 7 | "build": "concurrently -c auto \"npm run build-r\" \"npm run build-py\" \"tsc --noEmit\"", 8 | "watch": "concurrently -c auto \"npm run watch-r\" \"npm run watch-py\" \"tsc --noEmit --watch --preserveWatchOutput\"", 9 | "build-r": "esbuild srcts/main.tsx --bundle --minify --outfile=r/www/main.js --format=esm --alias:react=react", 10 | "watch-r": "esbuild srcts/main.tsx --bundle --minify --outfile=r/www/main.js --format=esm --alias:react=react --watch", 11 | "build-py": "esbuild srcts/main.tsx --bundle --minify --outfile=py/www/main.js --format=esm --alias:react=react", 12 | "watch-py": "esbuild srcts/main.tsx --bundle --minify --outfile=py/www/main.js --format=esm --alias:react=react --watch", 13 | "clean": "rm -rf r/www py/www" 14 | }, 15 | "author": "Winston Chang", 16 | "license": "MIT", 17 | "devDependencies": { 18 | "@types/react": "^19.1.7", 19 | "@types/react-dom": "^19.1.7", 20 | "concurrently": "^9.0.1", 21 | "esbuild": "^0.25.9", 22 | "prettier": "^3.6.2", 23 | "prettier-plugin-organize-imports": "^4.2.0", 24 | "react": "^19.1.1", 25 | "react-dom": "^19.1.1", 26 | "tsx": "^4.20.4", 27 | "typescript": "^5.9.2" 28 | }, 29 | "dependencies": { 30 | "@posit/shiny-react": "file:../.." 31 | }, 32 | "exampleMetadata": { 33 | "title": "Server Messages", 34 | "description": "Server-to-client messaging patterns and real-time updates", 35 | "deployToShinylive": true, 36 | "comment": "" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "**/__pycache__": true 5 | }, 6 | "search.exclude": { 7 | "node_modules": true 8 | }, 9 | "editor.formatOnSave": true, 10 | "editor.tabSize": 2, 11 | "files.encoding": "utf8", 12 | "files.eol": "\n", 13 | "files.trimTrailingWhitespace": true, 14 | "files.insertFinalNewline": true, 15 | "[markdown]": { 16 | "editor.formatOnSave": false, 17 | "files.trimTrailingWhitespace": false 18 | }, 19 | "[javascript]": { 20 | "editor.formatOnSave": true, 21 | "editor.defaultFormatter": "esbenp.prettier-vscode" 22 | }, 23 | "[typescript]": { 24 | "editor.formatOnSave": true, 25 | "editor.defaultFormatter": "esbenp.prettier-vscode" 26 | }, 27 | "[typescriptreact]": { 28 | "editor.formatOnSave": true, 29 | "editor.defaultFormatter": "esbenp.prettier-vscode" 30 | }, 31 | "[json]": { 32 | "editor.formatOnSave": true, 33 | "editor.defaultFormatter": "esbenp.prettier-vscode" 34 | }, 35 | "[html]": { 36 | "editor.formatOnSave": true, 37 | "editor.defaultFormatter": "esbenp.prettier-vscode" 38 | }, 39 | "[css]": { 40 | "editor.formatOnSave": true, 41 | "editor.defaultFormatter": "esbenp.prettier-vscode" 42 | }, 43 | "[python]": { 44 | "editor.defaultFormatter": "ms-python.black-formatter", 45 | "editor.formatOnSave": true, 46 | "editor.tabSize": 4, 47 | "editor.codeActionsOnSave": { 48 | "source.organizeImports": "explicit" 49 | } 50 | }, 51 | "prettier.prettierPath": "./node_modules/prettier", 52 | "files.associations": { 53 | "*.css": "tailwindcss" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/2-inputs/py/app.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from pathlib import Path 3 | from shiny import App, Inputs, Outputs, Session 4 | from shinyreact import page_react, render_json 5 | 6 | 7 | def server(input: Inputs, output: Outputs, session: Session): 8 | @render_json 9 | def txtout(): 10 | return input.txtin().upper() 11 | 12 | @render_json 13 | def numberout(): 14 | return str(input.numberin()) 15 | 16 | @render_json 17 | def checkboxout(): 18 | return str(input.checkboxin()) 19 | 20 | @render_json 21 | def radioout(): 22 | return str(input.radioin()) 23 | 24 | @render_json 25 | def selectout(): 26 | return str(input.selectin()) 27 | 28 | @render_json 29 | def sliderout(): 30 | return str(input.sliderin()) 31 | 32 | @render_json 33 | def dateout(): 34 | return str(input.datein()) 35 | 36 | # Track number of button clicks 37 | num_button_clicks = 0 38 | 39 | @render_json 40 | def buttonout(): 41 | if input.buttonin() is None: 42 | return None 43 | nonlocal num_button_clicks 44 | num_button_clicks += 1 45 | return str(num_button_clicks) 46 | 47 | @render_json 48 | def fileout(): 49 | return input.filein() 50 | 51 | @render_json 52 | def batchout(): 53 | data = input.batchdata() 54 | if data is None: 55 | return "No data submitted yet." 56 | 57 | data["receivedAt"] = datetime.datetime.now().isoformat() 58 | 59 | return data 60 | 61 | 62 | app = App( 63 | page_react(title="Inputs - Shiny React"), 64 | server, 65 | static_assets=str(Path(__file__).parent / "www"), 66 | ) 67 | -------------------------------------------------------------------------------- /scripts/build-all-examples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Get list of example directories 4 | EXAMPLE_DIRS=$(find examples -maxdepth 1 -type d -name "*-*" | sort) 5 | 6 | if [ -z "$EXAMPLE_DIRS" ]; then 7 | echo "No example directories found under ./examples." 8 | echo "Are you running this from the top level of the shiny-react repository?" 9 | echo "Expected to find directories like examples/1-hello-world/" 10 | echo "If not, cd to the project root (so that ./examples/ exists) and re-run: scripts/build-all-examples.sh" 11 | exit 1 12 | fi 13 | 14 | echo "Found example directories: $EXAMPLE_DIRS" 15 | 16 | START_DIR="$(pwd)" 17 | 18 | # Build shiny-react package first, because it's a dependency for all examples 19 | echo "Building shiny-react package..." 20 | if [ -f package-lock.json ]; then 21 | npm ci 22 | else 23 | npm install 24 | fi 25 | 26 | npm run build 27 | 28 | # Build all examples 29 | for dir in $EXAMPLE_DIRS; do 30 | echo "Building $dir..." 31 | cd "$dir" 32 | 33 | # Install dependencies with npm ci for faster, reliable builds 34 | if [ -f package-lock.json ]; then 35 | npm ci 36 | else 37 | npm install 38 | fi 39 | 40 | # Build for both R and Python 41 | if command -v jq >/dev/null 2>&1 && jq -e '.scripts["build-prod"]' package.json >/dev/null 2>&1; then 42 | echo "Found build-prod script; running production build" 43 | npm run build-prod 44 | else 45 | echo "No build-prod script; running default build" 46 | npm run build 47 | fi 48 | 49 | # Verify build outputs exist 50 | echo "Checking build outputs for $dir:" 51 | ls -la r/www/ || echo "No R www directory found" 52 | ls -la py/www/ || echo "No Python www directory found" 53 | 54 | cd "$START_DIR" 55 | done 56 | -------------------------------------------------------------------------------- /examples/2-inputs/srcts/ButtonInputCard.tsx: -------------------------------------------------------------------------------- 1 | import { useShinyInput, useShinyOutput } from "@posit/shiny-react"; 2 | import React from "react"; 3 | import InputOutputCard from "./InputOutputCard"; 4 | 5 | function ButtonInputCard() { 6 | const [buttonIn, setButtonIn] = useShinyInput( 7 | "buttonin", 8 | null, 9 | { 10 | // This makes it so there's no delay to sending the value when the button 11 | // is clicked. 12 | debounceMs: 0, 13 | // This makes it so that even if the input value is the same as the 14 | // previous, it will still cause invalidation of reactive functions on the 15 | // server. 16 | priority: "event", 17 | }, 18 | ); 19 | const [buttonOut, _] = useShinyOutput("buttonout", undefined); 20 | 21 | const handleButtonClick = () => { 22 | setButtonIn({}); 23 | }; 24 | 25 | return ( 26 | 30 | 37 |
38 | Button sends: {JSON.stringify(buttonIn)} 39 |
40 |
41 | Note: useShinyInput is called with priority:"event" so that even 42 | though the same value (an empty object) is sent every time the 43 | button is clicked, it will still cause reactive invalidation on the 44 | server. 45 |
46 |
47 | } 48 | outputValue={buttonOut ? buttonOut : "undefined"} 49 | /> 50 | ); 51 | } 52 | 53 | export default ButtonInputCard; 54 | -------------------------------------------------------------------------------- /examples/7-chat/srcts/hooks/useDragAndDrop.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from "react"; 2 | 3 | export function useDragAndDrop() { 4 | const [isDragOver, setIsDragOver] = useState(false); 5 | const [dragCounter, setDragCounter] = useState(0); 6 | 7 | const handleDragEnter = useCallback((e: React.DragEvent) => { 8 | e.preventDefault(); 9 | e.stopPropagation(); 10 | 11 | setDragCounter((prev) => prev + 1); 12 | 13 | if (e.dataTransfer.types.includes("Files")) { 14 | setIsDragOver(true); 15 | } 16 | }, []); 17 | 18 | const handleDragLeave = useCallback((e: React.DragEvent) => { 19 | e.preventDefault(); 20 | e.stopPropagation(); 21 | 22 | setDragCounter((prev) => { 23 | const newCount = prev - 1; 24 | if (newCount <= 0) { 25 | setIsDragOver(false); 26 | return 0; 27 | } 28 | return newCount; 29 | }); 30 | }, []); 31 | 32 | const handleDragOver = useCallback((e: React.DragEvent) => { 33 | e.preventDefault(); 34 | e.stopPropagation(); 35 | }, []); 36 | 37 | const handleDrop = useCallback( 38 | (e: React.DragEvent, onFilesDropped?: (files: FileList) => void) => { 39 | e.preventDefault(); 40 | e.stopPropagation(); 41 | setIsDragOver(false); 42 | setDragCounter(0); 43 | 44 | const files = e.dataTransfer.files; 45 | if (files.length > 0 && onFilesDropped) { 46 | onFilesDropped(files); 47 | } 48 | }, 49 | [], 50 | ); 51 | 52 | const resetDragState = useCallback(() => { 53 | setIsDragOver(false); 54 | setDragCounter(0); 55 | }, []); 56 | 57 | return { 58 | isDragOver, 59 | dragCounter, 60 | handleDragEnter, 61 | handleDragLeave, 62 | handleDragOver, 63 | handleDrop, 64 | resetDragState, 65 | }; 66 | } 67 | -------------------------------------------------------------------------------- /examples/3-outputs/py/mtcars.csv: -------------------------------------------------------------------------------- 1 | model,mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb 2 | Mazda RX4,21,6,160,110,3.9,2.62,16.46,0,1,4,4 3 | Mazda RX4 Wag,21,6,160,110,3.9,2.875,17.02,0,1,4,4 4 | Datsun 710,22.8,4,108,93,3.85,2.32,18.61,1,1,4,1 5 | Hornet 4 Drive,21.4,6,258,110,3.08,3.215,19.44,1,0,3,1 6 | Hornet Sportabout,18.7,8,360,175,3.15,3.44,17.02,0,0,3,2 7 | Valiant,18.1,6,225,105,2.76,3.46,20.22,1,0,3,1 8 | Duster 360,14.3,8,360,245,3.21,3.57,15.84,0,0,3,4 9 | Merc 240D,24.4,4,146.7,62,3.69,3.19,20,1,0,4,2 10 | Merc 230,22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2 11 | Merc 280,19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4 12 | Merc 280C,17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4 13 | Merc 450SE,16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3 14 | Merc 450SL,17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3 15 | Merc 450SLC,15.2,8,275.8,180,3.07,3.78,18,0,0,3,3 16 | Cadillac Fleetwood,10.4,8,472,205,2.93,5.25,17.98,0,0,3,4 17 | Lincoln Continental,10.4,8,460,215,3,5.424,17.82,0,0,3,4 18 | Chrysler Imperial,14.7,8,440,230,3.23,5.345,17.42,0,0,3,4 19 | Fiat 128,32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1 20 | Honda Civic,30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2 21 | Toyota Corolla,33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1 22 | Toyota Corona,21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1 23 | Dodge Challenger,15.5,8,318,150,2.76,3.52,16.87,0,0,3,2 24 | AMC Javelin,15.2,8,304,150,3.15,3.435,17.3,0,0,3,2 25 | Camaro Z28,13.3,8,350,245,3.73,3.84,15.41,0,0,3,4 26 | Pontiac Firebird,19.2,8,400,175,3.08,3.845,17.05,0,0,3,2 27 | Fiat X1-9,27.3,4,79,66,4.08,1.935,18.9,1,1,4,1 28 | Porsche 914-2,26,4,120.3,91,4.43,2.14,16.7,0,1,5,2 29 | Lotus Europa,30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2 30 | Ford Pantera L,15.8,8,351,264,4.22,3.17,14.5,0,1,5,4 31 | Ferrari Dino,19.7,6,145,175,3.62,2.77,15.5,0,1,5,6 32 | Maserati Bora,15,8,301,335,3.54,3.57,14.6,0,1,5,8 33 | Volvo 142E,21.4,4,121,109,4.11,2.78,18.6,1,1,4,2 34 | -------------------------------------------------------------------------------- /examples/3-outputs/r/mtcars.csv: -------------------------------------------------------------------------------- 1 | model,mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb 2 | Mazda RX4,21,6,160,110,3.9,2.62,16.46,0,1,4,4 3 | Mazda RX4 Wag,21,6,160,110,3.9,2.875,17.02,0,1,4,4 4 | Datsun 710,22.8,4,108,93,3.85,2.32,18.61,1,1,4,1 5 | Hornet 4 Drive,21.4,6,258,110,3.08,3.215,19.44,1,0,3,1 6 | Hornet Sportabout,18.7,8,360,175,3.15,3.44,17.02,0,0,3,2 7 | Valiant,18.1,6,225,105,2.76,3.46,20.22,1,0,3,1 8 | Duster 360,14.3,8,360,245,3.21,3.57,15.84,0,0,3,4 9 | Merc 240D,24.4,4,146.7,62,3.69,3.19,20,1,0,4,2 10 | Merc 230,22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2 11 | Merc 280,19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4 12 | Merc 280C,17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4 13 | Merc 450SE,16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3 14 | Merc 450SL,17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3 15 | Merc 450SLC,15.2,8,275.8,180,3.07,3.78,18,0,0,3,3 16 | Cadillac Fleetwood,10.4,8,472,205,2.93,5.25,17.98,0,0,3,4 17 | Lincoln Continental,10.4,8,460,215,3,5.424,17.82,0,0,3,4 18 | Chrysler Imperial,14.7,8,440,230,3.23,5.345,17.42,0,0,3,4 19 | Fiat 128,32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1 20 | Honda Civic,30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2 21 | Toyota Corolla,33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1 22 | Toyota Corona,21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1 23 | Dodge Challenger,15.5,8,318,150,2.76,3.52,16.87,0,0,3,2 24 | AMC Javelin,15.2,8,304,150,3.15,3.435,17.3,0,0,3,2 25 | Camaro Z28,13.3,8,350,245,3.73,3.84,15.41,0,0,3,4 26 | Pontiac Firebird,19.2,8,400,175,3.08,3.845,17.05,0,0,3,2 27 | Fiat X1-9,27.3,4,79,66,4.08,1.935,18.9,1,1,4,1 28 | Porsche 914-2,26,4,120.3,91,4.43,2.14,16.7,0,1,5,2 29 | Lotus Europa,30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2 30 | Ford Pantera L,15.8,8,351,264,4.22,3.17,14.5,0,1,5,4 31 | Ferrari Dino,19.7,6,145,175,3.62,2.77,15.5,0,1,5,6 32 | Maserati Bora,15,8,301,335,3.54,3.57,14.6,0,1,5,8 33 | Volvo 142E,21.4,4,121,109,4.11,2.78,18.6,1,1,4,2 34 | -------------------------------------------------------------------------------- /examples/7-chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shiny-react-chat", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "AI chat application using shiny-react, ellmer (R), chatlas (Python), and shadcn/ui", 6 | "scripts": { 7 | "build": "concurrently -c auto \"tsc --noEmit\" \"tsx build.ts\"", 8 | "watch": "concurrently -c auto \"tsc --noEmit --watch --preserveWatchOutput\" \"tsx build.ts --watch\"", 9 | "build-prod": "tsc --noEmit && tsx build.ts --production", 10 | "clean": "rm -rf r/www py/www build" 11 | }, 12 | "author": "Winston Chang", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/react": "^19.1.1", 16 | "@types/react-dom": "^19.1.1", 17 | "autoprefixer": "^10.4.16", 18 | "chokidar": "^4.0.3", 19 | "concurrently": "^9.2.1", 20 | "esbuild": "^0.25.9", 21 | "esbuild-plugin-tailwindcss": "^2.1.0", 22 | "prettier": "^3.6.2", 23 | "prettier-plugin-organize-imports": "^4.2.0", 24 | "react": "^19.1.1", 25 | "react-dom": "^19.1.1", 26 | "tailwindcss": "^4.1.12", 27 | "tsx": "^4.20.4", 28 | "typescript": "^5.9.2" 29 | }, 30 | "dependencies": { 31 | "@posit/shiny-react": "file:../..", 32 | "@radix-ui/react-avatar": "^1.0.4", 33 | "@radix-ui/react-scroll-area": "^1.0.5", 34 | "@radix-ui/react-separator": "^1.0.3", 35 | "@radix-ui/react-slot": "^1.0.2", 36 | "class-variance-authority": "^0.7.1", 37 | "clsx": "^2.1.1", 38 | "lucide-react": "^0.542.0", 39 | "tailwind-merge": "^3.3.1" 40 | }, 41 | "exampleMetadata": { 42 | "title": "AI Chat", 43 | "description": "AI chat application with LLM integration", 44 | "deployToShinylive": false, 45 | "comment": "Not available to run in Shinylive because it requires packages that don't work in webR/Pyodide, and it needs an API key for LLM" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/7-chat/srcts/components/ui/scroll-area.tsx: -------------------------------------------------------------------------------- 1 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"; 2 | import * as React from "react"; 3 | 4 | import { cn } from "@/lib/utils"; 5 | 6 | const ScrollArea = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, children, ...props }, ref) => ( 10 | 15 | 16 | {children} 17 | 18 | 19 | 20 | 21 | )); 22 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; 23 | 24 | const ScrollBar = React.forwardRef< 25 | React.ElementRef, 26 | React.ComponentPropsWithoutRef 27 | >(({ className, orientation = "vertical", ...props }, ref) => ( 28 | 41 | 42 | 43 | )); 44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; 45 | 46 | export { ScrollArea, ScrollBar }; 47 | -------------------------------------------------------------------------------- /examples/2-inputs/srcts/RadioInputCard.tsx: -------------------------------------------------------------------------------- 1 | import { useShinyInput, useShinyOutput } from "@posit/shiny-react"; 2 | import React from "react"; 3 | import InputOutputCard from "./InputOutputCard"; 4 | 5 | function RadioInputCard() { 6 | const [radioIn, setRadioIn] = useShinyInput("radioin", "option1", { 7 | debounceMs: 0, 8 | }); 9 | const [radioOut, _] = useShinyOutput("radioout", undefined); 10 | 11 | const handleInputChange = (event: React.ChangeEvent) => { 12 | setRadioIn(event.target.value); 13 | }; 14 | 15 | return ( 16 | 20 | 31 | 42 | 53 | 54 | } 55 | outputValue={radioOut} 56 | /> 57 | ); 58 | } 59 | 60 | export default RadioInputCard; 61 | -------------------------------------------------------------------------------- /examples/5-shadcn/srcts/components/TextInputCard.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from "@/components/ui/badge"; 2 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 3 | import { Input } from "@/components/ui/input"; 4 | import { useShinyInput, useShinyOutput } from "@posit/shiny-react"; 5 | import React from "react"; 6 | 7 | export function TextInputCard() { 8 | const [inputText, setInputText] = useShinyInput("user_text", ""); 9 | const [processedText, processedTextRecalculating] = useShinyOutput( 10 | "processed_text", 11 | "", 12 | ); 13 | const [textLength, textLengthRecalculating] = useShinyOutput( 14 | "text_length", 15 | 0, 16 | ); 17 | 18 | const handleInputChange = (event: React.ChangeEvent) => { 19 | setInputText(event.target.value); 20 | }; 21 | 22 | return ( 23 | 24 | 25 | Text Input 26 | 27 | 28 |
29 | 35 | 42 |
43 |
44 |

45 | Processed text from server: 46 |

47 |
48 |
49 |               {processedText || "No text entered yet"}
50 |             
51 |
52 |
53 |
54 | Length: {textLength} 55 |
56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /examples/6-dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shiny-react-dashboard", 3 | "version": "1.0.0", 4 | "type": "module", 5 | "description": "Interactive dashboard example using shiny-react and shadcn/ui", 6 | "scripts": { 7 | "build": "concurrently -c auto \"tsc --noEmit\" \"tsx build.ts\"", 8 | "watch": "concurrently -c auto \"tsc --noEmit --watch --preserveWatchOutput\" \"tsx build.ts --watch\"", 9 | "build-prod": "tsc --noEmit && tsx build.ts --production", 10 | "clean": "rm -rf r/www py/www build" 11 | }, 12 | "author": "Winston Chang", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/react": "^19.1.1", 16 | "@types/react-dom": "^19.1.1", 17 | "autoprefixer": "^10.4.16", 18 | "chokidar": "^4.0.3", 19 | "concurrently": "^9.2.1", 20 | "esbuild": "^0.25.9", 21 | "esbuild-plugin-tailwindcss": "^2.1.0", 22 | "prettier": "^3.6.2", 23 | "prettier-plugin-organize-imports": "^4.2.0", 24 | "react": "^19.1.1", 25 | "react-dom": "^19.1.1", 26 | "tailwindcss": "^4.1.12", 27 | "tsx": "^4.20.4", 28 | "typescript": "^5.9.2" 29 | }, 30 | "dependencies": { 31 | "@posit/shiny-react": "file:../..", 32 | "@radix-ui/react-dialog": "^1.0.5", 33 | "@radix-ui/react-dropdown-menu": "^2.0.6", 34 | "@radix-ui/react-label": "^2.0.2", 35 | "@radix-ui/react-popover": "^1.0.7", 36 | "@radix-ui/react-scroll-area": "^1.0.5", 37 | "@radix-ui/react-select": "^2.0.0", 38 | "@radix-ui/react-separator": "^1.0.3", 39 | "@radix-ui/react-slot": "^1.0.2", 40 | "@radix-ui/react-tabs": "^1.0.4", 41 | "@radix-ui/react-tooltip": "^1.0.7", 42 | "class-variance-authority": "^0.7.1", 43 | "clsx": "^2.1.1", 44 | "lucide-react": "^0.542.0", 45 | "recharts": "^2.8.0", 46 | "tailwind-merge": "^3.3.1" 47 | }, 48 | "exampleMetadata": { 49 | "title": "Dashboard", 50 | "description": "Full dashboard with charts, tables, and interactive widgets", 51 | "deployToShinylive": true, 52 | "comment": "" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/3-outputs/srcts/DataTableCard.tsx: -------------------------------------------------------------------------------- 1 | import { useShinyOutput } from "@posit/shiny-react"; 2 | import React from "react"; 3 | import Card from "./Card"; 4 | 5 | function DataTableCard() { 6 | const [tableData, isRecalculating] = useShinyOutput< 7 | Record | undefined 8 | >("table_data", undefined); 9 | 10 | // Get column names from the data 11 | const columnNames = tableData ? Object.keys(tableData) : []; 12 | 13 | // Get number of rows from first column 14 | const numRows = 15 | columnNames.length > 0 && tableData ? tableData[columnNames[0]].length : 0; 16 | 17 | return ( 18 | 19 |
20 |

{numRows} rows from mtcars dataset

21 |
25 | 26 | 27 | 28 | {columnNames.map((colName) => ( 29 | 30 | ))} 31 | 32 | 33 | 34 | {Array.from({ length: numRows }, (_, rowIndex) => ( 35 | 36 | {columnNames.map((colName) => { 37 | const value = tableData?.[colName][rowIndex]; 38 | return ( 39 | 46 | ); 47 | })} 48 | 49 | ))} 50 | 51 |
{colName.toUpperCase()}
40 | {typeof value === "number" 41 | ? Number.isInteger(value) 42 | ? value 43 | : value.toFixed(3) 44 | : value} 45 |
52 |
53 |
54 |
55 | ); 56 | } 57 | 58 | export default DataTableCard; 59 | -------------------------------------------------------------------------------- /examples/5-shadcn/srcts/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | }, 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button"; 45 | return ( 46 | 51 | ); 52 | }, 53 | ); 54 | Button.displayName = "Button"; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /examples/6-dashboard/srcts/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | }, 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button"; 45 | return ( 46 | 51 | ); 52 | }, 53 | ); 54 | Button.displayName = "Button"; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /examples/1-hello-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "shiny-react-hello-world", 4 | "version": "1.0.0", 5 | "type": "module", 6 | "description": "Hello World app using shiny-react", 7 | "scripts": { 8 | "dev": "concurrently -c auto \"npm run watch\" \"npm run shinyapp\"", 9 | "build": "concurrently -c auto \"npm run build-r\" \"npm run build-py\" \"tsc --noEmit\"", 10 | "watch": "concurrently -c auto \"npm run watch-r\" \"npm run watch-py\" \"tsc --noEmit --watch --preserveWatchOutput\"", 11 | "shinyapp": "concurrently -c auto \"npm run shinyapp-r\" \"npm run shinyapp-py\"", 12 | "dev-r": "concurrently -c auto \"npm run watch-r\" \"npm run shinyapp-r\"", 13 | "dev-py": "concurrently -c auto \"npm run watch-py\" \"npm run shinyapp-py\"", 14 | "build-r": "esbuild srcts/main.tsx --bundle --minify --outfile=r/www/main.js --format=esm --alias:react=react", 15 | "build-py": "esbuild srcts/main.tsx --bundle --minify --outfile=py/www/main.js --format=esm --alias:react=react", 16 | "watch-r": "esbuild srcts/main.tsx --bundle --minify --outfile=r/www/main.js --format=esm --alias:react=react --watch", 17 | "watch-py": "esbuild srcts/main.tsx --bundle --minify --outfile=py/www/main.js --format=esm --alias:react=react --watch", 18 | "shinyapp-r": "Rscript -e \"options(shiny.autoreload = TRUE); shiny::runApp('r/app.R', port=${R_PORT:-8000})\"", 19 | "shinyapp-py": "cd py && shiny run app.py --reload --port ${PY_PORT:-8001}", 20 | "clean": "rm -rf r/www py/www" 21 | }, 22 | "author": "Winston Chang", 23 | "license": "MIT", 24 | "devDependencies": { 25 | "@types/react": "^19.1.12", 26 | "@types/react-dom": "^19.1.9", 27 | "concurrently": "^9.0.1", 28 | "esbuild": "^0.25.9", 29 | "react": "^19.1.1", 30 | "react-dom": "^19.1.1", 31 | "typescript": "^5.9.2" 32 | }, 33 | "dependencies": { 34 | "@posit/shiny-react": "file:../.." 35 | }, 36 | "exampleMetadata": { 37 | "title": "Hello World", 38 | "description": "Basic bidirectional communication between React and Shiny", 39 | "deployToShinylive": true, 40 | "comment": "" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/2-inputs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "shiny-react-inputs", 4 | "version": "1.0.0", 5 | "type": "module", 6 | "description": "Inputs example app using shiny-react", 7 | "scripts": { 8 | "dev": "concurrently -c auto \"npm run watch\" \"npm run shinyapp\"", 9 | "build": "concurrently -c auto \"npm run build-r\" \"npm run build-py\" \"tsc --noEmit\"", 10 | "watch": "concurrently -c auto \"npm run watch-r\" \"npm run watch-py\" \"tsc --noEmit --watch --preserveWatchOutput\"", 11 | "shinyapp": "concurrently -c auto \"npm run shinyapp-r\" \"npm run shinyapp-py\"", 12 | "dev-r": "concurrently -c auto \"npm run watch-r\" \"npm run shinyapp-r\"", 13 | "dev-py": "concurrently -c auto \"npm run watch-py\" \"npm run shinyapp-py\"", 14 | "build-r": "esbuild srcts/main.tsx --bundle --minify --outfile=r/www/main.js --format=esm --alias:react=react", 15 | "build-py": "esbuild srcts/main.tsx --bundle --minify --outfile=py/www/main.js --format=esm --alias:react=react", 16 | "watch-r": "esbuild srcts/main.tsx --bundle --outfile=r/www/main.js --sourcemap --format=esm --alias:react=react --watch", 17 | "watch-py": "esbuild srcts/main.tsx --bundle --outfile=py/www/main.js --sourcemap --format=esm --alias:react=react --watch", 18 | "shinyapp-r": "Rscript -e \"options(shiny.autoreload = TRUE); shiny::runApp('r/app.R', port=${R_PORT:-8000})\"", 19 | "shinyapp-py": "cd py && shiny run app.py --reload --port ${PY_PORT:-8001}", 20 | "clean": "rm -rf r/www py/www" 21 | }, 22 | "author": "Winston Chang", 23 | "license": "MIT", 24 | "devDependencies": { 25 | "@types/react": "^19.1.12", 26 | "@types/react-dom": "^19.1.9", 27 | "concurrently": "^9.0.1", 28 | "esbuild": "^0.25.9", 29 | "react": "^19.1.1", 30 | "react-dom": "^19.1.1", 31 | "typescript": "^5.9.2" 32 | }, 33 | "dependencies": { 34 | "@posit/shiny-react": "file:../.." 35 | }, 36 | "exampleMetadata": { 37 | "title": "Input Components", 38 | "description": "Comprehensive showcase of input components and form handling", 39 | "deployToShinylive": true, 40 | "comment": "" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /examples/7-chat/r/shinyreact.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | 3 | page_bare <- function(..., title = NULL, lang = NULL) { 4 | ui <- list( 5 | shiny:::jqueryDependency(), 6 | if (!is.null(title)) tags$head(tags$title(title)), 7 | ... 8 | ) 9 | attr(ui, "lang") <- lang 10 | ui 11 | } 12 | 13 | page_react <- function( 14 | ..., 15 | title = NULL, 16 | js_file = "main.js", 17 | css_file = "main.css", 18 | lang = "en" 19 | ) { 20 | page_bare( 21 | title = title, 22 | tags$head( 23 | if (!is.null(js_file)) tags$script(src = js_file, type = "module"), 24 | if (!is.null(css_file)) tags$link(href = css_file, rel = "stylesheet") 25 | ), 26 | tags$div(id = "root"), 27 | ... 28 | ) 29 | } 30 | 31 | 32 | #' Reactively render arbitrary JSON object data. 33 | #' 34 | #' This is a generic renderer that can be used to render any Jsonifiable data. 35 | #' The data goes through shiny:::toJSON() before being sent to the client. 36 | render_json <- function( 37 | expr, 38 | env = parent.frame(), 39 | quoted = FALSE, 40 | outputArgs = list(), 41 | sep = " " 42 | ) { 43 | func <- installExprFunction( 44 | expr, 45 | "func", 46 | env, 47 | quoted, 48 | label = "render_json" 49 | ) 50 | 51 | createRenderFunction( 52 | func, 53 | function(value, session, name, ...) { 54 | value 55 | }, 56 | function(...) { 57 | stop("Not implemented") 58 | }, 59 | outputArgs 60 | ) 61 | } 62 | 63 | #' Send a custom message to the client 64 | #' 65 | #' A convenience function for sending custom messages from the Shiny server to 66 | #' React components using useShinyMessageHandler() hook. This wraps messages in a 67 | #' standard format and sends them via the "shinyReactMessage" channel. 68 | #' 69 | #' @param session The Shiny session object 70 | #' @param type The message type (should match messageType in useShinyMessageHandler) 71 | #' @param data The data to send to the client 72 | post_message <- function(session, type, data) { 73 | session$sendCustomMessage( 74 | "shinyReactMessage", 75 | list( 76 | type = type, 77 | data = data 78 | ) 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /examples/2-inputs/r/shinyreact.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | 3 | page_bare <- function(..., title = NULL, lang = NULL) { 4 | ui <- list( 5 | shiny:::jqueryDependency(), 6 | if (!is.null(title)) tags$head(tags$title(title)), 7 | ... 8 | ) 9 | attr(ui, "lang") <- lang 10 | ui 11 | } 12 | 13 | page_react <- function( 14 | ..., 15 | title = NULL, 16 | js_file = "main.js", 17 | css_file = "main.css", 18 | lang = "en" 19 | ) { 20 | page_bare( 21 | title = title, 22 | tags$head( 23 | if (!is.null(js_file)) tags$script(src = js_file, type = "module"), 24 | if (!is.null(css_file)) tags$link(href = css_file, rel = "stylesheet") 25 | ), 26 | tags$div(id = "root"), 27 | ... 28 | ) 29 | } 30 | 31 | 32 | #' Reactively render arbitrary JSON object data. 33 | #' 34 | #' This is a generic renderer that can be used to render any Jsonifiable data. 35 | #' The data goes through shiny:::toJSON() before being sent to the client. 36 | render_json <- function( 37 | expr, 38 | env = parent.frame(), 39 | quoted = FALSE, 40 | outputArgs = list(), 41 | sep = " " 42 | ) { 43 | func <- installExprFunction( 44 | expr, 45 | "func", 46 | env, 47 | quoted, 48 | label = "render_json" 49 | ) 50 | 51 | createRenderFunction( 52 | func, 53 | function(value, session, name, ...) { 54 | value 55 | }, 56 | function(...) { 57 | stop("Not implemented") 58 | }, 59 | outputArgs 60 | ) 61 | } 62 | 63 | #' Send a custom message to the client 64 | #' 65 | #' A convenience function for sending custom messages from the Shiny server to 66 | #' React components using useShinyMessageHandler() hook. This wraps messages in a 67 | #' standard format and sends them via the "shinyReactMessage" channel. 68 | #' 69 | #' @param session The Shiny session object 70 | #' @param type The message type (should match messageType in useShinyMessageHandler) 71 | #' @param data The data to send to the client 72 | post_message <- function(session, type, data) { 73 | session$sendCustomMessage( 74 | "shinyReactMessage", 75 | list( 76 | type = type, 77 | data = data 78 | ) 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /examples/3-outputs/r/shinyreact.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | 3 | page_bare <- function(..., title = NULL, lang = NULL) { 4 | ui <- list( 5 | shiny:::jqueryDependency(), 6 | if (!is.null(title)) tags$head(tags$title(title)), 7 | ... 8 | ) 9 | attr(ui, "lang") <- lang 10 | ui 11 | } 12 | 13 | page_react <- function( 14 | ..., 15 | title = NULL, 16 | js_file = "main.js", 17 | css_file = "main.css", 18 | lang = "en" 19 | ) { 20 | page_bare( 21 | title = title, 22 | tags$head( 23 | if (!is.null(js_file)) tags$script(src = js_file, type = "module"), 24 | if (!is.null(css_file)) tags$link(href = css_file, rel = "stylesheet") 25 | ), 26 | tags$div(id = "root"), 27 | ... 28 | ) 29 | } 30 | 31 | 32 | #' Reactively render arbitrary JSON object data. 33 | #' 34 | #' This is a generic renderer that can be used to render any Jsonifiable data. 35 | #' The data goes through shiny:::toJSON() before being sent to the client. 36 | render_json <- function( 37 | expr, 38 | env = parent.frame(), 39 | quoted = FALSE, 40 | outputArgs = list(), 41 | sep = " " 42 | ) { 43 | func <- installExprFunction( 44 | expr, 45 | "func", 46 | env, 47 | quoted, 48 | label = "render_json" 49 | ) 50 | 51 | createRenderFunction( 52 | func, 53 | function(value, session, name, ...) { 54 | value 55 | }, 56 | function(...) { 57 | stop("Not implemented") 58 | }, 59 | outputArgs 60 | ) 61 | } 62 | 63 | #' Send a custom message to the client 64 | #' 65 | #' A convenience function for sending custom messages from the Shiny server to 66 | #' React components using useShinyMessageHandler() hook. This wraps messages in a 67 | #' standard format and sends them via the "shinyReactMessage" channel. 68 | #' 69 | #' @param session The Shiny session object 70 | #' @param type The message type (should match messageType in useShinyMessageHandler) 71 | #' @param data The data to send to the client 72 | post_message <- function(session, type, data) { 73 | session$sendCustomMessage( 74 | "shinyReactMessage", 75 | list( 76 | type = type, 77 | data = data 78 | ) 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /examples/4-messages/r/shinyreact.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | 3 | page_bare <- function(..., title = NULL, lang = NULL) { 4 | ui <- list( 5 | shiny:::jqueryDependency(), 6 | if (!is.null(title)) tags$head(tags$title(title)), 7 | ... 8 | ) 9 | attr(ui, "lang") <- lang 10 | ui 11 | } 12 | 13 | page_react <- function( 14 | ..., 15 | title = NULL, 16 | js_file = "main.js", 17 | css_file = "main.css", 18 | lang = "en" 19 | ) { 20 | page_bare( 21 | title = title, 22 | tags$head( 23 | if (!is.null(js_file)) tags$script(src = js_file, type = "module"), 24 | if (!is.null(css_file)) tags$link(href = css_file, rel = "stylesheet") 25 | ), 26 | tags$div(id = "root"), 27 | ... 28 | ) 29 | } 30 | 31 | 32 | #' Reactively render arbitrary JSON object data. 33 | #' 34 | #' This is a generic renderer that can be used to render any Jsonifiable data. 35 | #' The data goes through shiny:::toJSON() before being sent to the client. 36 | render_json <- function( 37 | expr, 38 | env = parent.frame(), 39 | quoted = FALSE, 40 | outputArgs = list(), 41 | sep = " " 42 | ) { 43 | func <- installExprFunction( 44 | expr, 45 | "func", 46 | env, 47 | quoted, 48 | label = "render_json" 49 | ) 50 | 51 | createRenderFunction( 52 | func, 53 | function(value, session, name, ...) { 54 | value 55 | }, 56 | function(...) { 57 | stop("Not implemented") 58 | }, 59 | outputArgs 60 | ) 61 | } 62 | 63 | #' Send a custom message to the client 64 | #' 65 | #' A convenience function for sending custom messages from the Shiny server to 66 | #' React components using useShinyMessageHandler() hook. This wraps messages in a 67 | #' standard format and sends them via the "shinyReactMessage" channel. 68 | #' 69 | #' @param session The Shiny session object 70 | #' @param type The message type (should match messageType in useShinyMessageHandler) 71 | #' @param data The data to send to the client 72 | post_message <- function(session, type, data) { 73 | session$sendCustomMessage( 74 | "shinyReactMessage", 75 | list( 76 | type = type, 77 | data = data 78 | ) 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /examples/5-shadcn/r/shinyreact.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | 3 | page_bare <- function(..., title = NULL, lang = NULL) { 4 | ui <- list( 5 | shiny:::jqueryDependency(), 6 | if (!is.null(title)) tags$head(tags$title(title)), 7 | ... 8 | ) 9 | attr(ui, "lang") <- lang 10 | ui 11 | } 12 | 13 | page_react <- function( 14 | ..., 15 | title = NULL, 16 | js_file = "main.js", 17 | css_file = "main.css", 18 | lang = "en" 19 | ) { 20 | page_bare( 21 | title = title, 22 | tags$head( 23 | if (!is.null(js_file)) tags$script(src = js_file, type = "module"), 24 | if (!is.null(css_file)) tags$link(href = css_file, rel = "stylesheet") 25 | ), 26 | tags$div(id = "root"), 27 | ... 28 | ) 29 | } 30 | 31 | 32 | #' Reactively render arbitrary JSON object data. 33 | #' 34 | #' This is a generic renderer that can be used to render any Jsonifiable data. 35 | #' The data goes through shiny:::toJSON() before being sent to the client. 36 | render_json <- function( 37 | expr, 38 | env = parent.frame(), 39 | quoted = FALSE, 40 | outputArgs = list(), 41 | sep = " " 42 | ) { 43 | func <- installExprFunction( 44 | expr, 45 | "func", 46 | env, 47 | quoted, 48 | label = "render_json" 49 | ) 50 | 51 | createRenderFunction( 52 | func, 53 | function(value, session, name, ...) { 54 | value 55 | }, 56 | function(...) { 57 | stop("Not implemented") 58 | }, 59 | outputArgs 60 | ) 61 | } 62 | 63 | #' Send a custom message to the client 64 | #' 65 | #' A convenience function for sending custom messages from the Shiny server to 66 | #' React components using useShinyMessageHandler() hook. This wraps messages in a 67 | #' standard format and sends them via the "shinyReactMessage" channel. 68 | #' 69 | #' @param session The Shiny session object 70 | #' @param type The message type (should match messageType in useShinyMessageHandler) 71 | #' @param data The data to send to the client 72 | post_message <- function(session, type, data) { 73 | session$sendCustomMessage( 74 | "shinyReactMessage", 75 | list( 76 | type = type, 77 | data = data 78 | ) 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /examples/6-dashboard/r/shinyreact.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | 3 | page_bare <- function(..., title = NULL, lang = NULL) { 4 | ui <- list( 5 | shiny:::jqueryDependency(), 6 | if (!is.null(title)) tags$head(tags$title(title)), 7 | ... 8 | ) 9 | attr(ui, "lang") <- lang 10 | ui 11 | } 12 | 13 | page_react <- function( 14 | ..., 15 | title = NULL, 16 | js_file = "main.js", 17 | css_file = "main.css", 18 | lang = "en" 19 | ) { 20 | page_bare( 21 | title = title, 22 | tags$head( 23 | if (!is.null(js_file)) tags$script(src = js_file, type = "module"), 24 | if (!is.null(css_file)) tags$link(href = css_file, rel = "stylesheet") 25 | ), 26 | tags$div(id = "root"), 27 | ... 28 | ) 29 | } 30 | 31 | 32 | #' Reactively render arbitrary JSON object data. 33 | #' 34 | #' This is a generic renderer that can be used to render any Jsonifiable data. 35 | #' The data goes through shiny:::toJSON() before being sent to the client. 36 | render_json <- function( 37 | expr, 38 | env = parent.frame(), 39 | quoted = FALSE, 40 | outputArgs = list(), 41 | sep = " " 42 | ) { 43 | func <- installExprFunction( 44 | expr, 45 | "func", 46 | env, 47 | quoted, 48 | label = "render_json" 49 | ) 50 | 51 | createRenderFunction( 52 | func, 53 | function(value, session, name, ...) { 54 | value 55 | }, 56 | function(...) { 57 | stop("Not implemented") 58 | }, 59 | outputArgs 60 | ) 61 | } 62 | 63 | #' Send a custom message to the client 64 | #' 65 | #' A convenience function for sending custom messages from the Shiny server to 66 | #' React components using useShinyMessageHandler() hook. This wraps messages in a 67 | #' standard format and sends them via the "shinyReactMessage" channel. 68 | #' 69 | #' @param session The Shiny session object 70 | #' @param type The message type (should match messageType in useShinyMessageHandler) 71 | #' @param data The data to send to the client 72 | post_message <- function(session, type, data) { 73 | session$sendCustomMessage( 74 | "shinyReactMessage", 75 | list( 76 | type = type, 77 | data = data 78 | ) 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /examples/1-hello-world/r/shinyreact.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | 3 | page_bare <- function(..., title = NULL, lang = NULL) { 4 | ui <- list( 5 | shiny:::jqueryDependency(), 6 | if (!is.null(title)) tags$head(tags$title(title)), 7 | ... 8 | ) 9 | attr(ui, "lang") <- lang 10 | ui 11 | } 12 | 13 | page_react <- function( 14 | ..., 15 | title = NULL, 16 | js_file = "main.js", 17 | css_file = "main.css", 18 | lang = "en" 19 | ) { 20 | page_bare( 21 | title = title, 22 | tags$head( 23 | if (!is.null(js_file)) tags$script(src = js_file, type = "module"), 24 | if (!is.null(css_file)) tags$link(href = css_file, rel = "stylesheet") 25 | ), 26 | tags$div(id = "root"), 27 | ... 28 | ) 29 | } 30 | 31 | 32 | #' Reactively render arbitrary JSON object data. 33 | #' 34 | #' This is a generic renderer that can be used to render any Jsonifiable data. 35 | #' The data goes through shiny:::toJSON() before being sent to the client. 36 | render_json <- function( 37 | expr, 38 | env = parent.frame(), 39 | quoted = FALSE, 40 | outputArgs = list(), 41 | sep = " " 42 | ) { 43 | func <- installExprFunction( 44 | expr, 45 | "func", 46 | env, 47 | quoted, 48 | label = "render_json" 49 | ) 50 | 51 | createRenderFunction( 52 | func, 53 | function(value, session, name, ...) { 54 | value 55 | }, 56 | function(...) { 57 | stop("Not implemented") 58 | }, 59 | outputArgs 60 | ) 61 | } 62 | 63 | #' Send a custom message to the client 64 | #' 65 | #' A convenience function for sending custom messages from the Shiny server to 66 | #' React components using useShinyMessageHandler() hook. This wraps messages in a 67 | #' standard format and sends them via the "shinyReactMessage" channel. 68 | #' 69 | #' @param session The Shiny session object 70 | #' @param type The message type (should match messageType in useShinyMessageHandler) 71 | #' @param data The data to send to the client 72 | post_message <- function(session, type, data) { 73 | session$sendCustomMessage( 74 | "shinyReactMessage", 75 | list( 76 | type = type, 77 | data = data 78 | ) 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /examples/7-chat/srcts/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | }, 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button"; 45 | return ( 46 | 51 | ); 52 | }, 53 | ); 54 | Button.displayName = "Button"; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /examples/7-chat/srcts/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLHeadingElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )); 45 | CardTitle.displayName = "CardTitle"; 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )); 57 | CardDescription.displayName = "CardDescription"; 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )); 65 | CardContent.displayName = "CardContent"; 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )); 77 | CardFooter.displayName = "CardFooter"; 78 | 79 | export { 80 | Card, 81 | CardContent, 82 | CardDescription, 83 | CardFooter, 84 | CardHeader, 85 | CardTitle, 86 | }; 87 | -------------------------------------------------------------------------------- /examples/5-shadcn/srcts/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )); 45 | CardTitle.displayName = "CardTitle"; 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )); 57 | CardDescription.displayName = "CardDescription"; 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )); 65 | CardContent.displayName = "CardContent"; 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )); 77 | CardFooter.displayName = "CardFooter"; 78 | 79 | export { 80 | Card, 81 | CardContent, 82 | CardDescription, 83 | CardFooter, 84 | CardHeader, 85 | CardTitle, 86 | }; 87 | -------------------------------------------------------------------------------- /examples/6-dashboard/srcts/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )); 45 | CardTitle.displayName = "CardTitle"; 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )); 57 | CardDescription.displayName = "CardDescription"; 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )); 65 | CardContent.displayName = "CardContent"; 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )); 77 | CardFooter.displayName = "CardFooter"; 78 | 79 | export { 80 | Card, 81 | CardContent, 82 | CardDescription, 83 | CardFooter, 84 | CardHeader, 85 | CardTitle, 86 | }; 87 | -------------------------------------------------------------------------------- /examples/5-shadcn/py/app.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from datetime import datetime 4 | from pathlib import Path 5 | 6 | import matplotlib 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | import pandas as pd 10 | from shiny import App, Inputs, Outputs, Session, reactive, render 11 | from shinyreact import page_react, render_json 12 | 13 | matplotlib.use("Agg") 14 | 15 | # Generate sample data 16 | sample_data = pd.DataFrame( 17 | { 18 | "id": range(1, 9), 19 | "age": [25, 30, 35, 28, 32, 27, 29, 33], 20 | "score": [85.5, 92.1, 88.3, 88.7, 95.2, 81.9, 87.4, 90.6], 21 | "category": ["A", "B", "A", "C", "B", "A", "C", "B"], 22 | } 23 | ) 24 | 25 | 26 | def server(input: Inputs, output: Outputs, session: Session): 27 | 28 | @render_json 29 | def processed_text(): 30 | text = input.user_text() if input.user_text() is not None else "" 31 | if text == "": 32 | return "" 33 | # Simple text processing - uppercase and reverse 34 | return "".join(reversed(text.upper())) 35 | 36 | @render_json 37 | def text_length(): 38 | text = input.user_text() if input.user_text() is not None else "" 39 | return str(len(text)) 40 | 41 | @render_json 42 | @reactive.event(input.button_trigger) 43 | def button_response(): 44 | # React to button trigger 45 | now = datetime.now() 46 | return f"Event received at: {now.strftime('%Y-%m-%d %H:%M:%S')}.{now.microsecond//10000:02d}" 47 | 48 | # Plot output 49 | @render.plot() 50 | def plot1(): 51 | fig, ax = plt.subplots() 52 | 53 | ax.scatter(sample_data["age"], sample_data["score"], s=30, alpha=0.7) 54 | 55 | # Add trend line 56 | z = np.polyfit(sample_data["age"], sample_data["score"], 1) 57 | p = np.poly1d(z) 58 | # Create sorted x values for smooth trend line 59 | x_trend = np.linspace(sample_data["age"].min(), sample_data["age"].max(), 100) 60 | ax.plot(x_trend, p(x_trend), "r--", linewidth=2, alpha=0.8) 61 | 62 | ax.set_xlabel("Age") 63 | ax.set_ylabel("Score") 64 | ax.set_title("Age vs Score") 65 | ax.grid(True, alpha=0.3) 66 | 67 | return fig 68 | 69 | 70 | app = App( 71 | page_react(title="Shiny + shadcn/ui Example"), 72 | server, 73 | static_assets=str(Path(__file__).parent / "www"), 74 | ) 75 | -------------------------------------------------------------------------------- /examples/3-outputs/py/app.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | import matplotlib 6 | import matplotlib.pyplot as plt 7 | import numpy as np 8 | import pandas as pd 9 | from shiny import App, Inputs, Outputs, Session, render 10 | from shinyreact import page_react, render_json 11 | 12 | matplotlib.use("Agg") 13 | 14 | mtcars = pd.read_csv(Path(__file__).parent / "mtcars.csv") 15 | 16 | 17 | def server(input: Inputs, output: Outputs, session: Session): 18 | 19 | @render_json 20 | def table_data(): 21 | num_rows = input.table_rows() 22 | # This produces a JSON object in column-major format, as in: 23 | # { 24 | # "mpg": [21, 21, 22.8, ...], 25 | # "cyl": [6, 6, 4, ...], 26 | # "disp": [160, 160, 108, ...], 27 | # ... 28 | # } 29 | return mtcars.head(num_rows).to_dict(orient="list") 30 | 31 | @render_json 32 | def table_stats(): 33 | num_rows = input.table_rows() 34 | mtcars_subset = mtcars.head(num_rows) 35 | 36 | # Return some summary statistics 37 | return { 38 | "colname": "mpg", 39 | "mean": float(mtcars_subset["mpg"].mean()), 40 | "median": float(mtcars_subset["mpg"].median()), 41 | "min": float(mtcars_subset["mpg"].min()), 42 | "max": float(mtcars_subset["mpg"].max()), 43 | } 44 | 45 | @render.plot() 46 | def plot1(): 47 | num_rows = input.table_rows() 48 | mtcars_subset = mtcars.head(num_rows) 49 | 50 | # Create a scatter plot of mpg vs wt 51 | fig, ax = plt.subplots(figsize=(8, 6)) 52 | ax.scatter( 53 | mtcars_subset["wt"], 54 | mtcars_subset["mpg"], 55 | color="steelblue", 56 | alpha=0.7, 57 | s=60, 58 | ) 59 | 60 | # Add a trend line 61 | z = np.polyfit(mtcars_subset["wt"], mtcars_subset["mpg"], 1) 62 | p = np.poly1d(z) 63 | ax.plot( 64 | mtcars_subset["wt"], p(mtcars_subset["wt"]), "r--", alpha=0.8, linewidth=2 65 | ) 66 | 67 | ax.set_xlabel("Weight (1000 lbs)") 68 | ax.set_ylabel("Miles per Gallon") 69 | ax.set_title(f"MPG vs Weight - {len(mtcars_subset)} cars") 70 | ax.grid(True, alpha=0.3) 71 | 72 | return fig 73 | 74 | 75 | app = App( 76 | page_react(title="Outputs - Shiny React"), 77 | server, 78 | static_assets=str(Path(__file__).parent / "www"), 79 | ) 80 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import tsParser from "@typescript-eslint/parser"; 2 | import reactEslint from "eslint-plugin-react"; 3 | import reactHooks from "eslint-plugin-react-hooks"; 4 | import { defineConfig } from "eslint/config"; 5 | import globals from "globals"; 6 | import path from "path"; 7 | import tseslint from "typescript-eslint"; 8 | import url from "url"; 9 | 10 | const __filename = url.fileURLToPath(new URL(import.meta.url)); 11 | const __dirname = path.dirname(__filename); 12 | 13 | const commonRules = { 14 | curly: ["warn", "multi-line"], 15 | eqeqeq: "warn", 16 | "no-throw-literal": "warn", 17 | semi: "warn", 18 | "@typescript-eslint/naming-convention": "off", 19 | "@typescript-eslint/no-empty-object-type": "off", 20 | "@typescript-eslint/no-unused-vars": "off", 21 | "@typescript-eslint/consistent-type-imports": "warn", 22 | "@typescript-eslint/no-floating-promises": "error", 23 | "@typescript-eslint/no-misused-promises": "error", 24 | }; 25 | 26 | export default defineConfig([ 27 | ...tseslint.configs.recommended, 28 | { 29 | ignores: ["dist", "**/*.d.ts"], 30 | }, 31 | { 32 | // JavaScript scripts - these are run by nodejs. 33 | files: ["eslint.config.js"], 34 | languageOptions: { 35 | globals: globals.node, 36 | }, 37 | rules: { 38 | ...commonRules, 39 | "@typescript-eslint/no-require-imports": "off", 40 | }, 41 | }, 42 | { 43 | // Browser/React TypeScript 44 | files: ["src/**/*.{ts,tsx}", "examples/**/*.{ts,tsx}"], 45 | ...reactEslint.configs.flat.recommended, 46 | ...reactEslint.configs.flat["jsx-runtime"], 47 | plugins: { 48 | react: reactEslint, 49 | "react-hooks": reactHooks, 50 | }, 51 | languageOptions: { 52 | parser: tsParser, 53 | ecmaVersion: 2022, 54 | sourceType: "module", 55 | globals: globals.browser, 56 | parserOptions: { 57 | tsconfigRootDir: __dirname, 58 | project: "tsconfig.json", 59 | ecmaFeatures: { 60 | jsx: true, 61 | }, 62 | }, 63 | }, 64 | settings: { 65 | react: { 66 | version: "detect", 67 | }, 68 | }, 69 | rules: { 70 | ...commonRules, 71 | "@typescript-eslint/naming-convention": [ 72 | "warn", 73 | { 74 | selector: "function", 75 | format: ["camelCase", "PascalCase"], 76 | }, 77 | ], 78 | "react-hooks/rules-of-hooks": "error", 79 | "react-hooks/exhaustive-deps": "warn", 80 | }, 81 | }, 82 | ]); 83 | -------------------------------------------------------------------------------- /examples/7-chat/srcts/hooks/useImageUpload.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | 3 | export interface ImageAttachment { 4 | name: string; 5 | content: string; // base64 encoded data 6 | type: string; // MIME type 7 | size: number; // file size in bytes 8 | } 9 | 10 | export const MAX_FILE_SIZE_MB = 5; 11 | export const SUPPORTED_IMAGE_TYPES = [ 12 | "image/jpeg", 13 | "image/png", 14 | "image/webp", 15 | "image/gif", 16 | ]; 17 | 18 | export function useImageUpload() { 19 | const validateFile = useCallback((file: File): string | null => { 20 | if (!SUPPORTED_IMAGE_TYPES.includes(file.type)) { 21 | return `File type ${file.type} is not supported. Please use JPEG, PNG, WebP, or GIF.`; 22 | } 23 | if (file.size > MAX_FILE_SIZE_MB * 1024 * 1024) { 24 | return `File size must be less than ${MAX_FILE_SIZE_MB}MB.`; 25 | } 26 | return null; 27 | }, []); 28 | 29 | const processFiles = useCallback( 30 | async (files: FileList | File[]): Promise => { 31 | const fileArray = Array.from(files); 32 | const newAttachments: ImageAttachment[] = []; 33 | 34 | for (const file of fileArray) { 35 | const error = validateFile(file); 36 | if (error) { 37 | alert(error); // In production, you'd want better error handling 38 | continue; 39 | } 40 | 41 | // Convert to base64 42 | const reader = new FileReader(); 43 | const base64Promise = new Promise((resolve) => { 44 | reader.onload = () => { 45 | const result = reader.result as string; 46 | // Remove the data:image/...;base64, prefix 47 | const base64 = result.split(",")[1]; 48 | resolve(base64); 49 | }; 50 | }); 51 | 52 | reader.readAsDataURL(file); 53 | const base64Content = await base64Promise; 54 | 55 | newAttachments.push({ 56 | name: file.name, 57 | content: base64Content, 58 | type: file.type, 59 | size: file.size, 60 | }); 61 | } 62 | 63 | return newAttachments; 64 | }, 65 | [validateFile], 66 | ); 67 | 68 | const formatFileSize = useCallback((bytes: number) => { 69 | if (bytes < 1024) return bytes + " B"; 70 | if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"; 71 | return (bytes / (1024 * 1024)).toFixed(1) + " MB"; 72 | }, []); 73 | 74 | return { 75 | validateFile, 76 | processFiles, 77 | formatFileSize, 78 | MAX_FILE_SIZE_MB, 79 | SUPPORTED_IMAGE_TYPES, 80 | }; 81 | } 82 | -------------------------------------------------------------------------------- /examples/7-chat/srcts/components/ImagePreview.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { ImageAttachment, useImageUpload } from "@/hooks/useImageUpload"; 3 | import { X } from "lucide-react"; 4 | import React from "react"; 5 | 6 | interface ImagePreviewProps { 7 | attachments: ImageAttachment[]; 8 | onRemove: (index: number) => void; 9 | isLoading?: boolean; 10 | className?: string; 11 | columns?: 1 | 2 | 3 | 4 | 5 | 6; 12 | } 13 | 14 | export function ImagePreview({ 15 | attachments, 16 | onRemove, 17 | isLoading = false, 18 | className = "", 19 | columns = 4, 20 | }: ImagePreviewProps) { 21 | const { formatFileSize } = useImageUpload(); 22 | 23 | if (attachments.length === 0) { 24 | return null; 25 | } 26 | 27 | const gridColsClass = { 28 | 1: "grid-cols-1", 29 | 2: "grid-cols-2", 30 | 3: "grid-cols-3", 31 | 4: "grid-cols-4", 32 | 5: "grid-cols-5", 33 | 6: "grid-cols-6", 34 | }[columns]; 35 | 36 | return ( 37 |
38 |
39 | {attachments.length} image{attachments.length !== 1 ? "s" : ""} attached 40 |
41 |
42 | {attachments.map((attachment, index) => ( 43 |
47 |
48 | {attachment.name} 53 | 63 |
64 |
65 |
69 | {attachment.name} 70 |
71 |
72 | {formatFileSize(attachment.size)} 73 |
74 |
75 |
76 | ))} 77 |
78 |
79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /examples/3-outputs/srcts/StatisticsCard.tsx: -------------------------------------------------------------------------------- 1 | import { useShinyOutput } from "@posit/shiny-react"; 2 | import React from "react"; 3 | import Card from "./Card"; 4 | 5 | interface TableStats { 6 | colname: string; 7 | mean: number; 8 | median: number; 9 | min: number; 10 | max: number; 11 | } 12 | 13 | function StatisticsCard() { 14 | const [tableStats, isRecalculating] = useShinyOutput( 15 | "table_stats", 16 | undefined, 17 | ); 18 | 19 | return ( 20 | 21 |
22 | {tableStats ? ( 23 |
24 |

{tableStats.colname} statistics

25 |
26 |
27 |
28 |
29 | 30 | {tableStats.min.toFixed(1)} 31 | 32 | 33 | {tableStats.max.toFixed(1)} 34 | 35 |
36 |
43 |
44 |
45 | Mean 46 |
47 | {tableStats.mean.toFixed(1)} 48 |
49 |
50 |
57 |
58 |
59 | Median 60 |
61 | {tableStats.median.toFixed(1)} 62 |
63 |
64 |
65 |
66 |
67 |
68 | ) : ( 69 |
Loading statistics...
70 | )} 71 |
72 |
73 | ); 74 | } 75 | 76 | export default StatisticsCard; 77 | -------------------------------------------------------------------------------- /examples/1-hello-world/README.md: -------------------------------------------------------------------------------- 1 | # Hello World Example 2 | 3 | This is a simple example demonstrating how to use the shiny-react library to create React components that communicate with Shiny applications. 4 | 5 | The front end is implemented with React and TypeScript. There are two versions of the Shiny back end: one is implemented with R, and the other with Python. 6 | 7 | The front end uses `useShinyInput` and `useShinyOutput` hooks to send and receive values from the Shiny back end. The back end is a Shiny application that uses `render_json` to send the output values to the front end as JSON. In this example, the Shiny back end simply capitalizes the input value and sends it back to the front end. 8 | 9 | ## Directory Structure 10 | 11 | - **`r/`** - R Shiny application 12 | - `app.R` - Main R Shiny server application 13 | - `shinyreact.R` - R utility functions 14 | - **`py/`** - Python Shiny application 15 | - `app.py` - Main Python Shiny server application 16 | - `shinyreact.py` - Python utility functions 17 | - **`srcts/`** - TypeScript/React source code 18 | - `main.tsx` - Entry point that renders the React app 19 | - `HelloWorldComponent.tsx` - Main React component using shiny-react hooks 20 | - `styles.css` - Simple CSS styling for the application 21 | - **`r/www/`** - Built JavaScript output for R Shiny app (generated) 22 | - **`py/www/`** - Built JavaScript output for Python Shiny app (generated) 23 | - **`node_modules/`** - npm dependencies (generated) 24 | 25 | ## Building 26 | 27 | 1. Install dependencies: 28 | ```bash 29 | npm install 30 | ``` 31 | 32 | 2. Build the React application: 33 | ```bash 34 | npm run build 35 | ``` 36 | 37 | The build process compiles the TypeScript React code and CSS into JavaScript bundles output directly to `r/www/main.js` and `py/www/main.js`. The CSS is automatically bundled into the JavaScript files. 38 | 39 | Or for development with watch mode: 40 | ```bash 41 | npm run watch 42 | ``` 43 | 44 | The watch mode runs three processes concurrently: 45 | - TypeScript type checking in watch mode 46 | - ESBuild bundling for R app (outputs to `r/www/main.js`) 47 | - ESBuild bundling for Python app (outputs to `py/www/main.js`) 48 | 49 | Note that if you build just an R or Python Shiny application (instead of both, as in this example), then you can simplify the `build` and `watch` scripts in `package.json` to only target one output directory. 50 | 51 | 3. Run either the R or Python Shiny application: 52 | 53 | ```bash 54 | # For R 55 | R -e "options(shiny.autoreload = TRUE); shiny::runApp('r/app.R', port=8000)" 56 | 57 | # For Python 58 | shiny run py/app.py --port 8000 59 | ``` 60 | 61 | The commands above use port 8000, but you can use a different port. 62 | 63 | 4. Open your web browser and navigate to `http://localhost:8000` to see the Shiny-React application in action. 64 | -------------------------------------------------------------------------------- /examples/6-dashboard/py/app.py: -------------------------------------------------------------------------------- 1 | from shiny import App, Inputs, Outputs, Session, ui, reactive 2 | from shinyreact import page_react, render_json 3 | from data import generate_sample_data, filter_data, calculate_metrics 4 | from pathlib import Path 5 | 6 | # Generate sample data once when app starts 7 | sample_data = generate_sample_data() 8 | 9 | 10 | def server(input: Inputs, output: Outputs, session: Session): 11 | 12 | @reactive.calc 13 | def filtered_data(): 14 | """Reactive data filtering""" 15 | # Get input values with defaults 16 | date_range = ( 17 | input.date_range() if input.date_range() is not None else "last_30_days" 18 | ) 19 | search_term = input.search_term() if input.search_term() is not None else "" 20 | selected_categories = ( 21 | input.selected_categories() 22 | if input.selected_categories() is not None 23 | else [] 24 | ) 25 | 26 | return filter_data( 27 | sample_data, 28 | date_range=date_range, 29 | search_term=search_term, 30 | selected_categories=selected_categories, 31 | ) 32 | 33 | @render_json 34 | def metrics_data(): 35 | """Calculate and return metrics""" 36 | data = filtered_data() 37 | return calculate_metrics(data) 38 | 39 | @render_json 40 | def chart_data(): 41 | """Return chart data in column-major format""" 42 | data = filtered_data() 43 | 44 | # Convert DataFrames to column-major format (dict with column arrays) 45 | revenue_trend_columns = data["revenue_trend"].to_dict("list") 46 | category_performance_columns = data["category_performance"].to_dict("list") 47 | 48 | return { 49 | "revenue_trend": revenue_trend_columns, 50 | "category_performance": category_performance_columns, 51 | } 52 | 53 | @render_json 54 | def table_data(): 55 | """Return table data in column-major format""" 56 | data = filtered_data() 57 | 58 | # Sort products by revenue (descending) and take top 10 59 | products = data["products"].copy() 60 | if len(products) > 0: 61 | products_sorted = products.sort_values("revenue", ascending=False) 62 | top_products = products_sorted.head(10) 63 | 64 | # Convert to column-major format (dict with column arrays) 65 | columns_data = top_products.to_dict("list") 66 | else: 67 | # Return empty columns with correct structure 68 | columns_data = { 69 | "id": [], 70 | "product": [], 71 | "category": [], 72 | "sales": [], 73 | "revenue": [], 74 | "growth": [], 75 | "status": [], 76 | } 77 | 78 | return {"columns": columns_data, "total_rows": len(data["products"])} 79 | 80 | 81 | app = App( 82 | page_react(title="Dashboard - Shiny React"), 83 | server, 84 | static_assets=str(Path(__file__).parent / "www"), 85 | ) 86 | -------------------------------------------------------------------------------- /examples/6-dashboard/srcts/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { FilterPanel } from "@/components/FilterPanel"; 2 | import { Button } from "@/components/ui/button"; 3 | import { Separator } from "@/components/ui/separator"; 4 | import { cn } from "@/lib/utils"; 5 | import { 6 | BarChart3, 7 | HelpCircle, 8 | Home, 9 | Settings, 10 | ShoppingCart, 11 | TrendingUp, 12 | Users, 13 | } from "lucide-react"; 14 | import React from "react"; 15 | 16 | interface SidebarProps { 17 | className?: string; 18 | } 19 | 20 | const navigationItems = [ 21 | { 22 | title: "Dashboard", 23 | icon: Home, 24 | href: "#", 25 | active: true, 26 | }, 27 | { 28 | title: "Analytics", 29 | icon: BarChart3, 30 | href: "#", 31 | active: false, 32 | }, 33 | { 34 | title: "Customers", 35 | icon: Users, 36 | href: "#", 37 | active: false, 38 | }, 39 | { 40 | title: "Orders", 41 | icon: ShoppingCart, 42 | href: "#", 43 | active: false, 44 | }, 45 | { 46 | title: "Performance", 47 | icon: TrendingUp, 48 | href: "#", 49 | active: false, 50 | }, 51 | ]; 52 | 53 | const secondaryItems = [ 54 | { 55 | title: "Settings", 56 | icon: Settings, 57 | href: "#", 58 | }, 59 | { 60 | title: "Help", 61 | icon: HelpCircle, 62 | href: "#", 63 | }, 64 | ]; 65 | 66 | export function Sidebar({ className }: SidebarProps) { 67 | return ( 68 |
69 |
70 |
71 |

72 | Analytics Dashboard 73 |

74 |
75 | {navigationItems.map((item) => { 76 | const IconComponent = item.icon; 77 | return ( 78 | 86 | ); 87 | })} 88 |
89 |
90 | 91 |
92 |
93 | {secondaryItems.map((item) => { 94 | const IconComponent = item.icon; 95 | return ( 96 | 104 | ); 105 | })} 106 |
107 |
108 | 109 |
110 | 111 |
112 |
113 |
114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /examples/2-inputs/py/shinyreact.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from shiny import ui, Session 4 | from shiny.html_dependencies import shiny_deps 5 | from shiny.types import Jsonifiable 6 | from shiny.render.renderer import Renderer, ValueFn 7 | from typing import Any, Mapping, Optional, Sequence, Union 8 | 9 | 10 | def page_bare(*args: ui.TagChild, title: str | None = None, lang: str = "en") -> ui.Tag: 11 | return ui.tags.html( 12 | ui.tags.head(ui.tags.title(title)), 13 | ui.tags.body(shiny_deps(False), *args), 14 | lang=lang, 15 | ) 16 | 17 | 18 | def page_react( 19 | *args: ui.TagChild, 20 | title: str | None = None, 21 | js_file: str | None = "main.js", 22 | css_file: str | None = "main.css", 23 | lang: str = "en", 24 | ) -> ui.Tag: 25 | 26 | head_items: list[ui.TagChild] = [] 27 | 28 | if js_file: 29 | head_items.append(ui.tags.script(src=js_file, type="module")) 30 | if css_file: 31 | head_items.append(ui.tags.link(href=css_file, rel="stylesheet")) 32 | 33 | return page_bare( 34 | ui.head_content(*head_items), 35 | ui.div(id="root"), 36 | *args, 37 | title=title, 38 | lang=lang, 39 | ) 40 | 41 | 42 | class render_json(Renderer[Jsonifiable]): 43 | """ 44 | Reactively render arbitrary JSON object. 45 | 46 | This is a generic renderer that can be used to render any Jsonifiable data. 47 | It sends the data to the client-side and let the client-side code handle the 48 | rendering. 49 | 50 | Returns 51 | ------- 52 | : 53 | A decorator for a function that returns a Jsonifiable object. 54 | 55 | """ 56 | 57 | def __init__( 58 | self, 59 | _fn: Optional[ValueFn[Any]] = None, 60 | ) -> None: 61 | super().__init__(_fn) 62 | 63 | async def transform(self, value: Jsonifiable) -> Jsonifiable: 64 | return value 65 | 66 | 67 | # This is like Jsonifiable, but where Jsonifiable uses Dict, List, and Tuple, 68 | # this replaces those with Mapping and Sequence. Because Dict and List are 69 | # invariant, it can cause problems when a parameter is specified as Jsonifiable; 70 | # the replacements are covariant, which solves these problems. 71 | JsonifiableIn = Union[ 72 | str, 73 | int, 74 | float, 75 | bool, 76 | None, 77 | Sequence["JsonifiableIn"], 78 | "JsonifiableMapping", 79 | ] 80 | 81 | JsonifiableMapping = Mapping[str, JsonifiableIn] 82 | 83 | 84 | async def post_message(session: Session, type: str, data: JsonifiableIn): 85 | """ 86 | Send a custom message to the client. 87 | 88 | A convenience function for sending custom messages from the Shiny server to 89 | React components using useShinyMessageHandler() hook. This wraps messages in 90 | a standard format and sends them via the "shinyReactMessage" channel. 91 | 92 | Parameters 93 | ---------- 94 | session 95 | The Shiny session object 96 | type 97 | The message type (should match the messageType in 98 | useShinyMessageHandler) 99 | data 100 | The data to send to the client 101 | """ 102 | await session.send_custom_message("shinyReactMessage", {"type": type, "data": data}) 103 | -------------------------------------------------------------------------------- /examples/3-outputs/py/shinyreact.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from shiny import ui, Session 4 | from shiny.html_dependencies import shiny_deps 5 | from shiny.types import Jsonifiable 6 | from shiny.render.renderer import Renderer, ValueFn 7 | from typing import Any, Mapping, Optional, Sequence, Union 8 | 9 | 10 | def page_bare(*args: ui.TagChild, title: str | None = None, lang: str = "en") -> ui.Tag: 11 | return ui.tags.html( 12 | ui.tags.head(ui.tags.title(title)), 13 | ui.tags.body(shiny_deps(False), *args), 14 | lang=lang, 15 | ) 16 | 17 | 18 | def page_react( 19 | *args: ui.TagChild, 20 | title: str | None = None, 21 | js_file: str | None = "main.js", 22 | css_file: str | None = "main.css", 23 | lang: str = "en", 24 | ) -> ui.Tag: 25 | 26 | head_items: list[ui.TagChild] = [] 27 | 28 | if js_file: 29 | head_items.append(ui.tags.script(src=js_file, type="module")) 30 | if css_file: 31 | head_items.append(ui.tags.link(href=css_file, rel="stylesheet")) 32 | 33 | return page_bare( 34 | ui.head_content(*head_items), 35 | ui.div(id="root"), 36 | *args, 37 | title=title, 38 | lang=lang, 39 | ) 40 | 41 | 42 | class render_json(Renderer[Jsonifiable]): 43 | """ 44 | Reactively render arbitrary JSON object. 45 | 46 | This is a generic renderer that can be used to render any Jsonifiable data. 47 | It sends the data to the client-side and let the client-side code handle the 48 | rendering. 49 | 50 | Returns 51 | ------- 52 | : 53 | A decorator for a function that returns a Jsonifiable object. 54 | 55 | """ 56 | 57 | def __init__( 58 | self, 59 | _fn: Optional[ValueFn[Any]] = None, 60 | ) -> None: 61 | super().__init__(_fn) 62 | 63 | async def transform(self, value: Jsonifiable) -> Jsonifiable: 64 | return value 65 | 66 | 67 | # This is like Jsonifiable, but where Jsonifiable uses Dict, List, and Tuple, 68 | # this replaces those with Mapping and Sequence. Because Dict and List are 69 | # invariant, it can cause problems when a parameter is specified as Jsonifiable; 70 | # the replacements are covariant, which solves these problems. 71 | JsonifiableIn = Union[ 72 | str, 73 | int, 74 | float, 75 | bool, 76 | None, 77 | Sequence["JsonifiableIn"], 78 | "JsonifiableMapping", 79 | ] 80 | 81 | JsonifiableMapping = Mapping[str, JsonifiableIn] 82 | 83 | 84 | async def post_message(session: Session, type: str, data: JsonifiableIn): 85 | """ 86 | Send a custom message to the client. 87 | 88 | A convenience function for sending custom messages from the Shiny server to 89 | React components using useShinyMessageHandler() hook. This wraps messages in 90 | a standard format and sends them via the "shinyReactMessage" channel. 91 | 92 | Parameters 93 | ---------- 94 | session 95 | The Shiny session object 96 | type 97 | The message type (should match the messageType in 98 | useShinyMessageHandler) 99 | data 100 | The data to send to the client 101 | """ 102 | await session.send_custom_message("shinyReactMessage", {"type": type, "data": data}) 103 | -------------------------------------------------------------------------------- /examples/4-messages/py/shinyreact.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from shiny import ui, Session 4 | from shiny.html_dependencies import shiny_deps 5 | from shiny.types import Jsonifiable 6 | from shiny.render.renderer import Renderer, ValueFn 7 | from typing import Any, Mapping, Optional, Sequence, Union 8 | 9 | 10 | def page_bare(*args: ui.TagChild, title: str | None = None, lang: str = "en") -> ui.Tag: 11 | return ui.tags.html( 12 | ui.tags.head(ui.tags.title(title)), 13 | ui.tags.body(shiny_deps(False), *args), 14 | lang=lang, 15 | ) 16 | 17 | 18 | def page_react( 19 | *args: ui.TagChild, 20 | title: str | None = None, 21 | js_file: str | None = "main.js", 22 | css_file: str | None = "main.css", 23 | lang: str = "en", 24 | ) -> ui.Tag: 25 | 26 | head_items: list[ui.TagChild] = [] 27 | 28 | if js_file: 29 | head_items.append(ui.tags.script(src=js_file, type="module")) 30 | if css_file: 31 | head_items.append(ui.tags.link(href=css_file, rel="stylesheet")) 32 | 33 | return page_bare( 34 | ui.head_content(*head_items), 35 | ui.div(id="root"), 36 | *args, 37 | title=title, 38 | lang=lang, 39 | ) 40 | 41 | 42 | class render_json(Renderer[Jsonifiable]): 43 | """ 44 | Reactively render arbitrary JSON object. 45 | 46 | This is a generic renderer that can be used to render any Jsonifiable data. 47 | It sends the data to the client-side and let the client-side code handle the 48 | rendering. 49 | 50 | Returns 51 | ------- 52 | : 53 | A decorator for a function that returns a Jsonifiable object. 54 | 55 | """ 56 | 57 | def __init__( 58 | self, 59 | _fn: Optional[ValueFn[Any]] = None, 60 | ) -> None: 61 | super().__init__(_fn) 62 | 63 | async def transform(self, value: Jsonifiable) -> Jsonifiable: 64 | return value 65 | 66 | 67 | # This is like Jsonifiable, but where Jsonifiable uses Dict, List, and Tuple, 68 | # this replaces those with Mapping and Sequence. Because Dict and List are 69 | # invariant, it can cause problems when a parameter is specified as Jsonifiable; 70 | # the replacements are covariant, which solves these problems. 71 | JsonifiableIn = Union[ 72 | str, 73 | int, 74 | float, 75 | bool, 76 | None, 77 | Sequence["JsonifiableIn"], 78 | "JsonifiableMapping", 79 | ] 80 | 81 | JsonifiableMapping = Mapping[str, JsonifiableIn] 82 | 83 | 84 | async def post_message(session: Session, type: str, data: JsonifiableIn): 85 | """ 86 | Send a custom message to the client. 87 | 88 | A convenience function for sending custom messages from the Shiny server to 89 | React components using useShinyMessageHandler() hook. This wraps messages in 90 | a standard format and sends them via the "shinyReactMessage" channel. 91 | 92 | Parameters 93 | ---------- 94 | session 95 | The Shiny session object 96 | type 97 | The message type (should match the messageType in 98 | useShinyMessageHandler) 99 | data 100 | The data to send to the client 101 | """ 102 | await session.send_custom_message("shinyReactMessage", {"type": type, "data": data}) 103 | -------------------------------------------------------------------------------- /examples/5-shadcn/py/shinyreact.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from shiny import ui, Session 4 | from shiny.html_dependencies import shiny_deps 5 | from shiny.types import Jsonifiable 6 | from shiny.render.renderer import Renderer, ValueFn 7 | from typing import Any, Mapping, Optional, Sequence, Union 8 | 9 | 10 | def page_bare(*args: ui.TagChild, title: str | None = None, lang: str = "en") -> ui.Tag: 11 | return ui.tags.html( 12 | ui.tags.head(ui.tags.title(title)), 13 | ui.tags.body(shiny_deps(False), *args), 14 | lang=lang, 15 | ) 16 | 17 | 18 | def page_react( 19 | *args: ui.TagChild, 20 | title: str | None = None, 21 | js_file: str | None = "main.js", 22 | css_file: str | None = "main.css", 23 | lang: str = "en", 24 | ) -> ui.Tag: 25 | 26 | head_items: list[ui.TagChild] = [] 27 | 28 | if js_file: 29 | head_items.append(ui.tags.script(src=js_file, type="module")) 30 | if css_file: 31 | head_items.append(ui.tags.link(href=css_file, rel="stylesheet")) 32 | 33 | return page_bare( 34 | ui.head_content(*head_items), 35 | ui.div(id="root"), 36 | *args, 37 | title=title, 38 | lang=lang, 39 | ) 40 | 41 | 42 | class render_json(Renderer[Jsonifiable]): 43 | """ 44 | Reactively render arbitrary JSON object. 45 | 46 | This is a generic renderer that can be used to render any Jsonifiable data. 47 | It sends the data to the client-side and let the client-side code handle the 48 | rendering. 49 | 50 | Returns 51 | ------- 52 | : 53 | A decorator for a function that returns a Jsonifiable object. 54 | 55 | """ 56 | 57 | def __init__( 58 | self, 59 | _fn: Optional[ValueFn[Any]] = None, 60 | ) -> None: 61 | super().__init__(_fn) 62 | 63 | async def transform(self, value: Jsonifiable) -> Jsonifiable: 64 | return value 65 | 66 | 67 | # This is like Jsonifiable, but where Jsonifiable uses Dict, List, and Tuple, 68 | # this replaces those with Mapping and Sequence. Because Dict and List are 69 | # invariant, it can cause problems when a parameter is specified as Jsonifiable; 70 | # the replacements are covariant, which solves these problems. 71 | JsonifiableIn = Union[ 72 | str, 73 | int, 74 | float, 75 | bool, 76 | None, 77 | Sequence["JsonifiableIn"], 78 | "JsonifiableMapping", 79 | ] 80 | 81 | JsonifiableMapping = Mapping[str, JsonifiableIn] 82 | 83 | 84 | async def post_message(session: Session, type: str, data: JsonifiableIn): 85 | """ 86 | Send a custom message to the client. 87 | 88 | A convenience function for sending custom messages from the Shiny server to 89 | React components using useShinyMessageHandler() hook. This wraps messages in 90 | a standard format and sends them via the "shinyReactMessage" channel. 91 | 92 | Parameters 93 | ---------- 94 | session 95 | The Shiny session object 96 | type 97 | The message type (should match the messageType in 98 | useShinyMessageHandler) 99 | data 100 | The data to send to the client 101 | """ 102 | await session.send_custom_message("shinyReactMessage", {"type": type, "data": data}) 103 | -------------------------------------------------------------------------------- /examples/6-dashboard/py/shinyreact.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from shiny import ui, Session 4 | from shiny.html_dependencies import shiny_deps 5 | from shiny.types import Jsonifiable 6 | from shiny.render.renderer import Renderer, ValueFn 7 | from typing import Any, Mapping, Optional, Sequence, Union 8 | 9 | 10 | def page_bare(*args: ui.TagChild, title: str | None = None, lang: str = "en") -> ui.Tag: 11 | return ui.tags.html( 12 | ui.tags.head(ui.tags.title(title)), 13 | ui.tags.body(shiny_deps(False), *args), 14 | lang=lang, 15 | ) 16 | 17 | 18 | def page_react( 19 | *args: ui.TagChild, 20 | title: str | None = None, 21 | js_file: str | None = "main.js", 22 | css_file: str | None = "main.css", 23 | lang: str = "en", 24 | ) -> ui.Tag: 25 | 26 | head_items: list[ui.TagChild] = [] 27 | 28 | if js_file: 29 | head_items.append(ui.tags.script(src=js_file, type="module")) 30 | if css_file: 31 | head_items.append(ui.tags.link(href=css_file, rel="stylesheet")) 32 | 33 | return page_bare( 34 | ui.head_content(*head_items), 35 | ui.div(id="root"), 36 | *args, 37 | title=title, 38 | lang=lang, 39 | ) 40 | 41 | 42 | class render_json(Renderer[Jsonifiable]): 43 | """ 44 | Reactively render arbitrary JSON object. 45 | 46 | This is a generic renderer that can be used to render any Jsonifiable data. 47 | It sends the data to the client-side and let the client-side code handle the 48 | rendering. 49 | 50 | Returns 51 | ------- 52 | : 53 | A decorator for a function that returns a Jsonifiable object. 54 | 55 | """ 56 | 57 | def __init__( 58 | self, 59 | _fn: Optional[ValueFn[Any]] = None, 60 | ) -> None: 61 | super().__init__(_fn) 62 | 63 | async def transform(self, value: Jsonifiable) -> Jsonifiable: 64 | return value 65 | 66 | 67 | # This is like Jsonifiable, but where Jsonifiable uses Dict, List, and Tuple, 68 | # this replaces those with Mapping and Sequence. Because Dict and List are 69 | # invariant, it can cause problems when a parameter is specified as Jsonifiable; 70 | # the replacements are covariant, which solves these problems. 71 | JsonifiableIn = Union[ 72 | str, 73 | int, 74 | float, 75 | bool, 76 | None, 77 | Sequence["JsonifiableIn"], 78 | "JsonifiableMapping", 79 | ] 80 | 81 | JsonifiableMapping = Mapping[str, JsonifiableIn] 82 | 83 | 84 | async def post_message(session: Session, type: str, data: JsonifiableIn): 85 | """ 86 | Send a custom message to the client. 87 | 88 | A convenience function for sending custom messages from the Shiny server to 89 | React components using useShinyMessageHandler() hook. This wraps messages in 90 | a standard format and sends them via the "shinyReactMessage" channel. 91 | 92 | Parameters 93 | ---------- 94 | session 95 | The Shiny session object 96 | type 97 | The message type (should match the messageType in 98 | useShinyMessageHandler) 99 | data 100 | The data to send to the client 101 | """ 102 | await session.send_custom_message("shinyReactMessage", {"type": type, "data": data}) 103 | -------------------------------------------------------------------------------- /examples/1-hello-world/py/shinyreact.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from shiny import ui, Session 4 | from shiny.html_dependencies import shiny_deps 5 | from shiny.types import Jsonifiable 6 | from shiny.render.renderer import Renderer, ValueFn 7 | from typing import Any, Mapping, Optional, Sequence, Union 8 | 9 | 10 | def page_bare(*args: ui.TagChild, title: str | None = None, lang: str = "en") -> ui.Tag: 11 | return ui.tags.html( 12 | ui.tags.head(ui.tags.title(title)), 13 | ui.tags.body(shiny_deps(False), *args), 14 | lang=lang, 15 | ) 16 | 17 | 18 | def page_react( 19 | *args: ui.TagChild, 20 | title: str | None = None, 21 | js_file: str | None = "main.js", 22 | css_file: str | None = "main.css", 23 | lang: str = "en", 24 | ) -> ui.Tag: 25 | 26 | head_items: list[ui.TagChild] = [] 27 | 28 | if js_file: 29 | head_items.append(ui.tags.script(src=js_file, type="module")) 30 | if css_file: 31 | head_items.append(ui.tags.link(href=css_file, rel="stylesheet")) 32 | 33 | return page_bare( 34 | ui.head_content(*head_items), 35 | ui.div(id="root"), 36 | *args, 37 | title=title, 38 | lang=lang, 39 | ) 40 | 41 | 42 | class render_json(Renderer[Jsonifiable]): 43 | """ 44 | Reactively render arbitrary JSON object. 45 | 46 | This is a generic renderer that can be used to render any Jsonifiable data. 47 | It sends the data to the client-side and let the client-side code handle the 48 | rendering. 49 | 50 | Returns 51 | ------- 52 | : 53 | A decorator for a function that returns a Jsonifiable object. 54 | 55 | """ 56 | 57 | def __init__( 58 | self, 59 | _fn: Optional[ValueFn[Any]] = None, 60 | ) -> None: 61 | super().__init__(_fn) 62 | 63 | async def transform(self, value: Jsonifiable) -> Jsonifiable: 64 | return value 65 | 66 | 67 | # This is like Jsonifiable, but where Jsonifiable uses Dict, List, and Tuple, 68 | # this replaces those with Mapping and Sequence. Because Dict and List are 69 | # invariant, it can cause problems when a parameter is specified as Jsonifiable; 70 | # the replacements are covariant, which solves these problems. 71 | JsonifiableIn = Union[ 72 | str, 73 | int, 74 | float, 75 | bool, 76 | None, 77 | Sequence["JsonifiableIn"], 78 | "JsonifiableMapping", 79 | ] 80 | 81 | JsonifiableMapping = Mapping[str, JsonifiableIn] 82 | 83 | 84 | async def post_message(session: Session, type: str, data: JsonifiableIn): 85 | """ 86 | Send a custom message to the client. 87 | 88 | A convenience function for sending custom messages from the Shiny server to 89 | React components using useShinyMessageHandler() hook. This wraps messages in 90 | a standard format and sends them via the "shinyReactMessage" channel. 91 | 92 | Parameters 93 | ---------- 94 | session 95 | The Shiny session object 96 | type 97 | The message type (should match the messageType in 98 | useShinyMessageHandler) 99 | data 100 | The data to send to the client 101 | """ 102 | await session.send_custom_message("shinyReactMessage", {"type": type, "data": data}) 103 | -------------------------------------------------------------------------------- /examples/7-chat/py/shinyreact.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, Mapping, Optional, Sequence, Union 4 | 5 | from shiny import Session, ui 6 | from shiny.html_dependencies import shiny_deps 7 | from shiny.render.renderer import Renderer, ValueFn 8 | from shiny.types import Jsonifiable 9 | 10 | 11 | def page_bare(*args: ui.TagChild, title: str | None = None, lang: str = "en") -> ui.Tag: 12 | return ui.tags.html( 13 | ui.tags.head(ui.tags.title(title)), 14 | ui.tags.body(shiny_deps(False), *args), 15 | lang=lang, 16 | ) 17 | 18 | 19 | def page_react( 20 | *args: ui.TagChild, 21 | title: str | None = None, 22 | js_file: str | None = "main.js", 23 | css_file: str | None = "main.css", 24 | lang: str = "en", 25 | ) -> ui.Tag: 26 | 27 | head_items: list[ui.TagChild] = [] 28 | 29 | if js_file: 30 | head_items.append(ui.tags.script(src=js_file, type="module")) 31 | if css_file: 32 | head_items.append(ui.tags.link(href=css_file, rel="stylesheet")) 33 | 34 | return page_bare( 35 | ui.head_content(*head_items), 36 | ui.div(id="root"), 37 | *args, 38 | title=title, 39 | lang=lang, 40 | ) 41 | 42 | 43 | class render_json(Renderer[Jsonifiable]): 44 | """ 45 | Reactively render arbitrary JSON object. 46 | 47 | This is a generic renderer that can be used to render any Jsonifiable data. 48 | It sends the data to the client-side and let the client-side code handle the 49 | rendering. 50 | 51 | Returns 52 | ------- 53 | : 54 | A decorator for a function that returns a Jsonifiable object. 55 | 56 | """ 57 | 58 | def __init__( 59 | self, 60 | _fn: Optional[ValueFn[Any]] = None, 61 | ) -> None: 62 | super().__init__(_fn) 63 | 64 | async def transform(self, value: Jsonifiable) -> Jsonifiable: 65 | return value 66 | 67 | 68 | # This is like Jsonifiable, but where Jsonifiable uses Dict, List, and Tuple, 69 | # this replaces those with Mapping and Sequence. Because Dict and List are 70 | # invariant, it can cause problems when a parameter is specified as Jsonifiable; 71 | # the replacements are covariant, which solves these problems. 72 | JsonifiableIn = Union[ 73 | str, 74 | int, 75 | float, 76 | bool, 77 | None, 78 | Sequence["JsonifiableIn"], 79 | "JsonifiableMapping", 80 | ] 81 | 82 | JsonifiableMapping = Mapping[str, JsonifiableIn] 83 | 84 | 85 | async def post_message(session: Session, type: str, data: JsonifiableIn): 86 | """ 87 | Send a custom message to the client. 88 | 89 | A convenience function for sending custom messages from the Shiny server to 90 | React components using useShinyMessageHandler() hook. This wraps messages in 91 | a standard format and sends them via the "shinyReactMessage" channel. 92 | 93 | Parameters 94 | ---------- 95 | session 96 | The Shiny session object 97 | type 98 | The message type (should match the messageType in 99 | useShinyMessageHandler) 100 | data 101 | The data to send to the client 102 | """ 103 | await session.send_custom_message("shinyReactMessage", {"type": type, "data": data}) 104 | -------------------------------------------------------------------------------- /examples/6-dashboard/srcts/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )); 17 | Table.displayName = "Table"; 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )); 25 | TableHeader.displayName = "TableHeader"; 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )); 37 | TableBody.displayName = "TableBody"; 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes 42 | >(({ className, ...props }, ref) => ( 43 | tr]:last:border-b-0", className)} 46 | {...props} 47 | /> 48 | )); 49 | TableFooter.displayName = "TableFooter"; 50 | 51 | const TableRow = React.forwardRef< 52 | HTMLTableRowElement, 53 | React.HTMLAttributes 54 | >(({ className, ...props }, ref) => ( 55 | 63 | )); 64 | TableRow.displayName = "TableRow"; 65 | 66 | const TableHead = React.forwardRef< 67 | HTMLTableCellElement, 68 | React.ThHTMLAttributes 69 | >(({ className, ...props }, ref) => ( 70 |
78 | )); 79 | TableHead.displayName = "TableHead"; 80 | 81 | const TableCell = React.forwardRef< 82 | HTMLTableCellElement, 83 | React.TdHTMLAttributes 84 | >(({ className, ...props }, ref) => ( 85 | 90 | )); 91 | TableCell.displayName = "TableCell"; 92 | 93 | const TableCaption = React.forwardRef< 94 | HTMLTableCaptionElement, 95 | React.HTMLAttributes 96 | >(({ className, ...props }, ref) => ( 97 |
102 | )); 103 | TableCaption.displayName = "TableCaption"; 104 | 105 | export { 106 | Table, 107 | TableBody, 108 | TableCaption, 109 | TableCell, 110 | TableFooter, 111 | TableHead, 112 | TableHeader, 113 | TableRow, 114 | }; 115 | -------------------------------------------------------------------------------- /examples/7-chat/py/app.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import dotenv 4 | from chatlas import ChatOpenAI, content_image_url 5 | from shiny import App, Inputs, Outputs, Session, reactive 6 | 7 | from shinyreact import page_react 8 | 9 | # Load .env file in this directory for OPENAI_API_KEY 10 | app_dir = Path(__file__).parent 11 | env_file = app_dir / ".env" 12 | print(env_file) 13 | dotenv.load_dotenv(env_file) 14 | 15 | # Initialize chat with OpenAI GPT-4o-mini by default 16 | chat = ChatOpenAI( 17 | model="gpt-4o-mini", 18 | system_prompt="You are a helpful AI assistant. Be concise but informative in your responses.", 19 | ) 20 | 21 | 22 | def server(input: Inputs, output: Outputs, session: Session): 23 | 24 | @reactive.effect 25 | @reactive.event(input.chat_input) 26 | async def handle_chat_input(): 27 | message_data = input.chat_input() 28 | if not message_data or not message_data["text"]: 29 | return 30 | 31 | try: 32 | # Parse structured input (dict with text and attachments) 33 | # Handle both string (backwards compatibility) and structured input 34 | if isinstance(message_data, str): 35 | user_text = message_data.strip() 36 | attachments = [] 37 | elif isinstance(message_data, dict): 38 | user_text = message_data.get("text", "").strip() 39 | attachments = message_data.get("attachments", []) 40 | else: 41 | user_text = "" 42 | attachments = [] 43 | 44 | # Build chat arguments 45 | chat_args = [] 46 | 47 | # Add user text if present 48 | if user_text: 49 | chat_args.append(user_text) 50 | 51 | # Add image attachments as content_image_url objects 52 | if attachments: 53 | for attachment in attachments: 54 | if attachment.get("content") and attachment.get("type"): 55 | # Create data URL from base64 content 56 | data_url = ( 57 | f"data:{attachment['type']};base64,{attachment['content']}" 58 | ) 59 | # Add image content to chat arguments 60 | chat_args.append(content_image_url(data_url)) 61 | 62 | # Ensure we have at least some content to send 63 | if not chat_args: 64 | chat_args = ["Please provide some content to analyze."] 65 | 66 | # Create async streaming with all arguments 67 | stream = await chat.stream_async(*chat_args) 68 | async for chunk in stream: 69 | await send_chunk(chunk) 70 | 71 | await send_chunk("", done=True) 72 | 73 | except Exception as e: 74 | print(f"Error getting AI response: {e}") 75 | await send_chunk( 76 | "Sorry, I encountered an error processing your request. Please try again.", 77 | done=True, 78 | ) 79 | 80 | # Send a chunk of text to the front end 81 | async def send_chunk(chunk: str, done: bool = False): 82 | await session.send_custom_message("chat_stream", {"chunk": chunk, "done": done}) 83 | 84 | 85 | app = App( 86 | page_react(title="AI Chat - Shiny React"), 87 | server, 88 | static_assets=str(Path(__file__).parent / "www"), 89 | ) 90 | -------------------------------------------------------------------------------- /examples/7-chat/srcts/contexts/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useState } from "react"; 2 | 3 | export type ThemeName = 4 | | "default" 5 | | "paper" 6 | | "cyberpunk" 7 | | "glassmorphism" 8 | | "terminal" 9 | | "discord"; 10 | 11 | export interface Theme { 12 | name: ThemeName; 13 | displayName: string; 14 | description: string; 15 | previewColor: string; 16 | } 17 | 18 | export const themes: Theme[] = [ 19 | { 20 | name: "default", 21 | displayName: "Default", 22 | description: "Clean and minimal", 23 | previewColor: "bg-gray-100 border-gray-300", 24 | }, 25 | { 26 | name: "paper", 27 | displayName: "Paper", 28 | description: "Notebook and ink", 29 | previewColor: "bg-gray-50 border-blue-500", 30 | }, 31 | { 32 | name: "cyberpunk", 33 | displayName: "Cyberpunk", 34 | description: "Neon and electric", 35 | previewColor: "bg-gradient-to-r from-cyan-400 to-pink-500", 36 | }, 37 | { 38 | name: "glassmorphism", 39 | displayName: "Glass", 40 | description: "Frosted glass effects", 41 | previewColor: 42 | "bg-gradient-to-r from-blue-200/30 to-purple-200/30 backdrop-blur", 43 | }, 44 | { 45 | name: "terminal", 46 | displayName: "Terminal", 47 | description: "Retro computing", 48 | previewColor: "bg-black border-green-400", 49 | }, 50 | { 51 | name: "discord", 52 | displayName: "Discord", 53 | description: "Familiar chat style", 54 | previewColor: "bg-slate-800 border-purple-500", 55 | }, 56 | ]; 57 | 58 | interface ThemeContextType { 59 | currentTheme: ThemeName; 60 | setTheme: (theme: ThemeName) => void; 61 | getTheme: (name: ThemeName) => Theme; 62 | } 63 | 64 | const ThemeContext = createContext(undefined); 65 | 66 | export const useTheme = () => { 67 | const context = useContext(ThemeContext); 68 | if (context === undefined) { 69 | throw new Error("useTheme must be used within a ThemeProvider"); 70 | } 71 | return context; 72 | }; 73 | 74 | interface ThemeProviderProps { 75 | children: React.ReactNode; 76 | } 77 | 78 | export const ThemeProvider: React.FC = ({ children }) => { 79 | const [currentTheme, setCurrentTheme] = useState("default"); 80 | 81 | // Load theme from localStorage on mount 82 | useEffect(() => { 83 | const savedTheme = localStorage.getItem("chat-theme") as ThemeName; 84 | if (savedTheme && themes.find((t) => t.name === savedTheme)) { 85 | setCurrentTheme(savedTheme); 86 | } 87 | }, []); 88 | 89 | // Apply theme to document root 90 | useEffect(() => { 91 | const root = document.documentElement; 92 | 93 | // Remove all theme classes 94 | themes.forEach((theme) => { 95 | root.classList.remove(`theme-${theme.name}`); 96 | }); 97 | 98 | // Add current theme class 99 | root.classList.add(`theme-${currentTheme}`); 100 | }, [currentTheme]); 101 | 102 | const setTheme = (theme: ThemeName) => { 103 | setCurrentTheme(theme); 104 | localStorage.setItem("chat-theme", theme); 105 | }; 106 | 107 | const getTheme = (name: ThemeName) => { 108 | return themes.find((t) => t.name === name) || themes[0]; 109 | }; 110 | 111 | return ( 112 | 113 | {children} 114 | 115 | ); 116 | }; 117 | -------------------------------------------------------------------------------- /examples/2-inputs/srcts/FileInputCard.tsx: -------------------------------------------------------------------------------- 1 | import { useShinyOutput } from "@posit/shiny-react"; 2 | import React, { useRef, useState } from "react"; 3 | import InputOutputCard from "./InputOutputCard"; 4 | 5 | function FileInputCard() { 6 | const inputRef = useRef(null); 7 | const [files, setFiles] = useState([]); 8 | const [isDragOver, setIsDragOver] = useState(false); 9 | const [fileout, _] = useShinyOutput("fileout", undefined); 10 | 11 | const handleFiles = (files: FileList | null) => { 12 | if (files) { 13 | const fileArray = Array.from(files); 14 | setFiles(fileArray); 15 | } else { 16 | setFiles([]); 17 | } 18 | }; 19 | 20 | const handleInputChange = (event: React.ChangeEvent) => { 21 | handleFiles(event.target.files); 22 | }; 23 | 24 | const handleButtonClick = () => { 25 | inputRef.current?.click(); 26 | }; 27 | 28 | const handleDragOver = (event: React.DragEvent) => { 29 | event.preventDefault(); 30 | setIsDragOver(true); 31 | }; 32 | 33 | const handleDragLeave = (event: React.DragEvent) => { 34 | event.preventDefault(); 35 | setIsDragOver(false); 36 | }; 37 | 38 | const handleDrop = (event: React.DragEvent) => { 39 | event.preventDefault(); 40 | setIsDragOver(false); 41 | handleFiles(event.dataTransfer.files); 42 | }; 43 | 44 | const inputElement = ( 45 |
46 | {/* 47 | Hidden file input - Shiny will automatically detect this create a 48 | corresponding Shiny input with the same name as the id. 49 | */} 50 | 58 | 59 | {/* Custom drag and drop area */} 60 |
67 |
68 | {files.length === 0 ? ( 69 | <> 70 |
71 | Click to select files or drag and drop them here 72 |
73 |
Multiple files are supported
74 | 75 | ) : ( 76 |
77 |
    78 | {files.map((file, index) => ( 79 |
  • 80 | {file.name} ({Math.round(file.size / 1024)} KB) 81 |
  • 82 | ))} 83 |
84 |
85 | Click to select different files or drag new ones here 86 |
87 |
88 | )} 89 |
90 |
91 |
92 | ); 93 | const outputElement = ( 94 |
{JSON.stringify(fileout, null, 2)}
95 | ); 96 | 97 | return ( 98 | 104 | ); 105 | } 106 | 107 | export default FileInputCard; 108 | -------------------------------------------------------------------------------- /src/input-registry.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { type EventPriority } from "@posit/shiny/srcts/types/src/inputPolicies"; 3 | import { getShiny } from "./get-shiny"; 4 | import { createDebouncedFn, type DebouncedFunction } from "./utils"; 5 | 6 | export class InputRegistryEntry { 7 | id: string; // Shiny input ID 8 | value: T; 9 | useStateSetValueFns: Set<(value: T) => void>; 10 | shinySetInputValueDebounced: DebouncedFunction<(value: T) => void>; 11 | opts: { priority?: EventPriority; debounceMs: number } = { 12 | debounceMs: 100, 13 | }; 14 | 15 | constructor(id: string, value: T) { 16 | this.id = id; 17 | this.value = value; 18 | this.useStateSetValueFns = new Set(); 19 | this.shinySetInputValueDebounced = createDebouncedFn( 20 | this.setShinyInputValue.bind(this), 21 | this.opts.debounceMs, 22 | ); 23 | } 24 | 25 | isEmpty() { 26 | return this.useStateSetValueFns.size === 0; 27 | } 28 | 29 | private setShinyInputValue(value: T) { 30 | getShiny()?.setInputValue!(this.id, value, this.opts); 31 | } 32 | 33 | updateDebounceDelay(debounceMs: number) { 34 | this.shinySetInputValueDebounced.setDelay(debounceMs); 35 | } 36 | 37 | updatePriority(priority: EventPriority) { 38 | this.opts.priority = priority; 39 | } 40 | 41 | addUseStateSetValueFn(fn: (value: T) => void) { 42 | this.useStateSetValueFns.add(fn); 43 | } 44 | 45 | removeUseStateSetValueFn(fn: (value: T) => void) { 46 | this.useStateSetValueFns.delete(fn); 47 | } 48 | 49 | setValue(value: T) { 50 | this.value = value; 51 | this.shinySetInputValueDebounced(value); 52 | this.useStateSetValueFns.forEach((fn) => fn(value)); 53 | } 54 | 55 | getValue(): T { 56 | return this.value; 57 | } 58 | } 59 | 60 | export class InputRegistry { 61 | private inputs: Map> = new Map(); 62 | 63 | /** 64 | * Get an input registry entry by ID 65 | */ 66 | get(inputId: string): InputRegistryEntry | undefined { 67 | return this.inputs.get(inputId) as InputRegistryEntry | undefined; 68 | } 69 | 70 | /** 71 | * Check if an input registry entry exists 72 | */ 73 | has(inputId: string): boolean { 74 | return this.inputs.has(inputId); 75 | } 76 | 77 | /** 78 | * Add a new input registry entry 79 | */ 80 | add(inputId: string, value: T): InputRegistryEntry { 81 | if (this.inputs.has(inputId)) { 82 | throw new Error(`Input ${inputId} already exists`); 83 | } 84 | 85 | const entry = new InputRegistryEntry(inputId, value); 86 | this.inputs.set(inputId, entry); 87 | return entry; 88 | } 89 | 90 | /** 91 | * Get or create an input registry entry 92 | * 93 | * Note that value is used only if the entry is created; if it already exists, 94 | * then the existing entry is returned and the value is unused. 95 | */ 96 | getOrCreate(inputId: string, value: T): InputRegistryEntry { 97 | let entry = this.get(inputId); 98 | if (!entry) { 99 | entry = this.add(inputId, value); 100 | } 101 | return entry; 102 | } 103 | 104 | /** 105 | * Remove an input registry entry 106 | */ 107 | remove(inputId: string): boolean { 108 | return this.inputs.delete(inputId); 109 | } 110 | 111 | /** 112 | * Get all input IDs 113 | */ 114 | keys(): IterableIterator { 115 | return this.inputs.keys(); 116 | } 117 | 118 | /** 119 | * Get the number of registered inputs 120 | */ 121 | size(): number { 122 | return this.inputs.size; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /examples/6-dashboard/srcts/components/MetricsCards.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from "@/components/ui/badge"; 2 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 3 | import { Skeleton } from "@/components/ui/skeleton"; 4 | import { useShinyOutput } from "@posit/shiny-react"; 5 | import { 6 | Activity, 7 | DollarSign, 8 | ShoppingCart, 9 | TrendingDown, 10 | TrendingUp, 11 | Users, 12 | } from "lucide-react"; 13 | import React from "react"; 14 | 15 | interface Metric { 16 | title: string; 17 | value: string; 18 | change: number; 19 | trend: "up" | "down"; 20 | icon: React.ComponentType<{ className?: string }>; 21 | } 22 | 23 | interface MetricsData { 24 | revenue: Metric; 25 | users: Metric; 26 | orders: Metric; 27 | conversion: Metric; 28 | } 29 | 30 | export function MetricsCards() { 31 | const [metricsData, metricsDataRecalculating] = useShinyOutput< 32 | MetricsData | undefined 33 | >("metrics_data", undefined); 34 | 35 | if (!metricsData) { 36 | return ( 37 |
38 | {Array.from({ length: 4 }).map((_, i) => ( 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ))} 50 |
51 | ); 52 | } 53 | 54 | const metrics = [ 55 | { 56 | ...metricsData.revenue, 57 | icon: DollarSign, 58 | key: "revenue", 59 | }, 60 | { 61 | ...metricsData.users, 62 | icon: Users, 63 | key: "users", 64 | }, 65 | { 66 | ...metricsData.orders, 67 | icon: ShoppingCart, 68 | key: "orders", 69 | }, 70 | { 71 | ...metricsData.conversion, 72 | icon: Activity, 73 | key: "conversion", 74 | }, 75 | ]; 76 | 77 | return ( 78 |
79 | {metrics.map((metric) => { 80 | const IconComponent = metric.icon; 81 | return ( 82 | 83 | 84 | 85 | {metric.title} 86 | 87 | 88 | 89 | 90 |
{metric.value}
91 |
92 | 96 | {metric.trend === "up" ? ( 97 | 98 | ) : ( 99 | 100 | )} 101 | {Math.abs(metric.change)}% 102 | 103 | from last month 104 |
105 |
106 |
107 | ); 108 | })} 109 |
110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /examples/7-chat/r/app.R: -------------------------------------------------------------------------------- 1 | library(shiny) 2 | library(ellmer) 3 | dotenv::load_dot_env(".env") 4 | 5 | source("shinyreact.R", local = TRUE) 6 | # Initialize chat object - using OpenAI GPT-4o-mini by default 7 | # Users can set OPENAI_API_KEY environment variable or modify this 8 | chat <- 9 | chat_openai( 10 | "You are a helpful AI assistant. Be concise but informative in your responses.", 11 | model = "gpt-4o-mini" 12 | ) 13 | 14 | 15 | server <- function(input, output, session) { 16 | observeEvent(input$chat_input, { 17 | req(input$chat_input) 18 | req(input$chat_input$text) 19 | 20 | tryCatch( 21 | { 22 | # Parse structured input (JSON with text and attachments) 23 | message_data <- input$chat_input 24 | 25 | # Extract text - handle both string (backwards compatibility) and structured input 26 | user_text <- if (is.character(message_data)) { 27 | message_data 28 | } else if (is.list(message_data) && !is.null(message_data$text)) { 29 | message_data$text 30 | } else { 31 | "" 32 | } 33 | 34 | # Extract attachments if present 35 | attachments <- if ( 36 | is.list(message_data) && !is.null(message_data$attachments) 37 | ) { 38 | message_data$attachments 39 | } else { 40 | list() 41 | } 42 | 43 | # Build chat arguments 44 | chat_args <- list() 45 | 46 | # Add user text if present 47 | if (nzchar(user_text)) { 48 | chat_args <- append(chat_args, user_text) 49 | } 50 | 51 | # Add image attachments as content_image_url objects 52 | if (length(attachments) > 0) { 53 | for (i in seq_along(attachments)) { 54 | attachment <- attachments[[i]] 55 | if (!is.null(attachment$content) && !is.null(attachment$type)) { 56 | # Create data URL from base64 content 57 | data_url <- paste0( 58 | "data:", 59 | attachment$type, 60 | ";base64,", 61 | attachment$content 62 | ) 63 | # Add image content to chat arguments 64 | chat_args <- append( 65 | chat_args, 66 | list(ellmer::content_image_url(data_url)) 67 | ) 68 | } 69 | } 70 | } 71 | 72 | # Ensure we have at least some content to send 73 | if (length(chat_args) == 0) { 74 | chat_args <- list("Please provide some content to analyze.") 75 | } 76 | 77 | # Create async streaming with all arguments 78 | stream <- do.call(chat$stream_async, chat_args) 79 | coro::async(function() { 80 | for (chunk in await_each(stream)) { 81 | send_chunk(chunk) 82 | } 83 | 84 | # Send final message when streaming is complete 85 | send_chunk("", done = TRUE) 86 | })() 87 | }, 88 | error = function(e) { 89 | error_message <- paste("Error getting AI response:", e$message) 90 | warning(error_message) 91 | send_chunk( 92 | "Sorry, I encountered an error processing your request. Please try again.", 93 | done = TRUE 94 | ) 95 | } 96 | ) 97 | }) 98 | 99 | # Send a chunk of text to front end 100 | send_chunk <- function(chunk, done = FALSE) { 101 | session$sendCustomMessage( 102 | "chat_stream", 103 | list( 104 | chunk = chunk, 105 | done = done 106 | ) 107 | ) 108 | } 109 | } 110 | 111 | 112 | shinyApp( 113 | ui = page_react(title = "AI Chat - Shiny React"), 114 | server = server, 115 | ) 116 | -------------------------------------------------------------------------------- /examples/6-dashboard/build.ts: -------------------------------------------------------------------------------- 1 | import chokidar from "chokidar"; 2 | import * as esbuild from "esbuild"; 3 | import tailwindPlugin from "esbuild-plugin-tailwindcss"; 4 | 5 | const production = process.argv.includes("--production"); 6 | const watch = process.argv.includes("--watch"); 7 | const metafile = process.argv.includes("--metafile"); 8 | 9 | async function main() { 10 | const buildmap = { 11 | r: esbuild.context({ 12 | entryPoints: ["srcts/main.tsx"], 13 | outfile: "r/www/main.js", 14 | bundle: true, 15 | format: "esm", 16 | minify: production, 17 | sourcemap: production ? undefined : "linked", 18 | sourcesContent: true, 19 | alias: { 20 | react: "react", 21 | }, 22 | logLevel: "info", 23 | metafile: metafile, 24 | plugins: [tailwindPlugin()], 25 | }), 26 | py: esbuild.context({ 27 | entryPoints: ["srcts/main.tsx"], 28 | outfile: "py/www/main.js", 29 | bundle: true, 30 | format: "esm", 31 | minify: production, 32 | sourcemap: production ? undefined : "linked", 33 | sourcesContent: true, 34 | alias: { 35 | react: "react", 36 | }, 37 | logLevel: "info", 38 | metafile: metafile, 39 | plugins: [tailwindPlugin()], 40 | }), 41 | }; 42 | 43 | if (watch) { 44 | // Use chokidar for watching instead of esbuild's watch, because esbuild's 45 | // watch mode constantly consumes 15-25% CPU due to polling. 46 | // https://github.com/evanw/esbuild/issues/1527 47 | const contexts = await Promise.all(Object.values(buildmap)); 48 | 49 | // Initial build 50 | await Promise.all(contexts.map((context) => context.rebuild())); 51 | 52 | const watchPaths = ["srcts/", "tailwind.config.js"]; 53 | 54 | const watcher = chokidar.watch(watchPaths, { 55 | ignored: ["**/node_modules/**", "**/dist/**", "**/*.d.ts"], 56 | persistent: true, 57 | ignoreInitial: true, 58 | }); 59 | 60 | let rebuildTimeout: NodeJS.Timeout; 61 | 62 | watcher.on("all", (eventName, path) => { 63 | console.log(`${eventName}: ${path}`); 64 | 65 | // Debounce rebuilds to avoid rapid successive builds 66 | clearTimeout(rebuildTimeout); 67 | rebuildTimeout = setTimeout(async () => { 68 | try { 69 | await Promise.all(contexts.map((context) => context.rebuild())); 70 | } catch (error) { 71 | console.error("Rebuild failed:", error); 72 | } 73 | }, 100); 74 | }); 75 | 76 | watcher.on("error", (error) => { 77 | console.error("Watcher error:", error); 78 | }); 79 | 80 | // Graceful shutdown 81 | process.on("SIGINT", async () => { 82 | console.log("\nShutting down..."); 83 | 84 | // Close file watcher 85 | await watcher.close(); 86 | 87 | // Dispose esbuild contexts 88 | await Promise.all(contexts.map((context) => context.dispose())); 89 | 90 | process.exit(0); 91 | }); 92 | } else { 93 | // Non-watch build 94 | Object.entries(buildmap).forEach(([target, build]) => 95 | build 96 | .then(async (context: esbuild.BuildContext) => { 97 | console.log(`Building .js bundle for ${target} target...`); 98 | await context.rebuild(); 99 | console.log( 100 | `✓ Successfully built ${target === "py" ? "py/www/main.js" : "r/www/main.js"}` 101 | ); 102 | await context.dispose(); 103 | }) 104 | .catch((e) => { 105 | console.error(`Build failed for ${target} target:`, e); 106 | process.exit(1); 107 | }) 108 | ); 109 | } 110 | } 111 | 112 | main().catch((e) => { 113 | console.error(e); 114 | process.exit(1); 115 | }); -------------------------------------------------------------------------------- /examples/5-shadcn/build.ts: -------------------------------------------------------------------------------- 1 | import chokidar from "chokidar"; 2 | import * as esbuild from "esbuild"; 3 | import tailwindPlugin from "esbuild-plugin-tailwindcss"; 4 | 5 | const production = process.argv.includes("--production"); 6 | const watch = process.argv.includes("--watch"); 7 | const metafile = process.argv.includes("--metafile"); 8 | 9 | async function main() { 10 | const buildmap = { 11 | r: esbuild.context({ 12 | entryPoints: ["srcts/main.tsx"], 13 | outfile: "r/www/main.js", 14 | bundle: true, 15 | format: "esm", 16 | minify: production, 17 | sourcemap: production ? undefined : "linked", 18 | sourcesContent: true, 19 | alias: { 20 | react: "react", 21 | }, 22 | logLevel: "info", 23 | metafile: metafile, 24 | plugins: [tailwindPlugin()], 25 | }), 26 | py: esbuild.context({ 27 | entryPoints: ["srcts/main.tsx"], 28 | outfile: "py/www/main.js", 29 | bundle: true, 30 | format: "esm", 31 | minify: production, 32 | sourcemap: production ? undefined : "linked", 33 | sourcesContent: true, 34 | alias: { 35 | react: "react", 36 | }, 37 | logLevel: "info", 38 | metafile: metafile, 39 | plugins: [tailwindPlugin()], 40 | }), 41 | }; 42 | 43 | if (watch) { 44 | // Use chokidar for watching instead of esbuild's watch, because esbuild's 45 | // watch mode constantly consumes 15-25% CPU due to polling. 46 | // https://github.com/evanw/esbuild/issues/1527 47 | const contexts = await Promise.all(Object.values(buildmap)); 48 | 49 | // Initial build 50 | await Promise.all(contexts.map((context) => context.rebuild())); 51 | 52 | const watchPaths = ["srcts/", "tailwind.config.js"]; 53 | 54 | const watcher = chokidar.watch(watchPaths, { 55 | ignored: ["**/node_modules/**", "**/dist/**", "**/*.d.ts"], 56 | persistent: true, 57 | ignoreInitial: true, 58 | }); 59 | 60 | let rebuildTimeout: NodeJS.Timeout; 61 | 62 | watcher.on("all", (eventName, path) => { 63 | console.log(`${eventName}: ${path}`); 64 | 65 | // Debounce rebuilds to avoid rapid successive builds 66 | clearTimeout(rebuildTimeout); 67 | rebuildTimeout = setTimeout(async () => { 68 | try { 69 | await Promise.all(contexts.map((context) => context.rebuild())); 70 | } catch (error) { 71 | console.error("Rebuild failed:", error); 72 | } 73 | }, 100); 74 | }); 75 | 76 | watcher.on("error", (error) => { 77 | console.error("Watcher error:", error); 78 | }); 79 | 80 | // Graceful shutdown 81 | process.on("SIGINT", async () => { 82 | console.log("\nShutting down..."); 83 | 84 | // Close file watcher 85 | await watcher.close(); 86 | 87 | // Dispose esbuild contexts 88 | await Promise.all(contexts.map((context) => context.dispose())); 89 | 90 | process.exit(0); 91 | }); 92 | } else { 93 | // Non-watch build 94 | Object.entries(buildmap).forEach(([target, build]) => 95 | build 96 | .then(async (context: esbuild.BuildContext) => { 97 | console.log(`Building .js bundle for ${target} target...`); 98 | await context.rebuild(); 99 | console.log( 100 | `✓ Successfully built ${target === "py" ? "py/www/main.js" : "r/www/main.js"}` 101 | ); 102 | await context.dispose(); 103 | }) 104 | .catch((e) => { 105 | console.error(`Build failed for ${target} target:`, e); 106 | process.exit(1); 107 | }) 108 | ); 109 | } 110 | } 111 | 112 | main().catch((e) => { 113 | console.error(e); 114 | process.exit(1); 115 | }); 116 | -------------------------------------------------------------------------------- /examples/7-chat/build.ts: -------------------------------------------------------------------------------- 1 | import chokidar from "chokidar"; 2 | import * as esbuild from "esbuild"; 3 | import tailwindPlugin from "esbuild-plugin-tailwindcss"; 4 | 5 | const production = process.argv.includes("--production"); 6 | const watch = process.argv.includes("--watch"); 7 | const metafile = process.argv.includes("--metafile"); 8 | 9 | async function main() { 10 | const buildmap = { 11 | r: esbuild.context({ 12 | entryPoints: ["srcts/main.tsx"], 13 | outfile: "r/www/main.js", 14 | bundle: true, 15 | format: "esm", 16 | minify: production, 17 | sourcemap: production ? undefined : "linked", 18 | sourcesContent: true, 19 | alias: { 20 | react: "react", 21 | }, 22 | logLevel: "info", 23 | metafile: metafile, 24 | plugins: [tailwindPlugin()], 25 | }), 26 | py: esbuild.context({ 27 | entryPoints: ["srcts/main.tsx"], 28 | outfile: "py/www/main.js", 29 | bundle: true, 30 | format: "esm", 31 | minify: production, 32 | sourcemap: production ? undefined : "linked", 33 | sourcesContent: true, 34 | alias: { 35 | react: "react", 36 | }, 37 | logLevel: "info", 38 | metafile: metafile, 39 | plugins: [tailwindPlugin()], 40 | }), 41 | }; 42 | 43 | if (watch) { 44 | // Use chokidar for watching instead of esbuild's watch, because esbuild's 45 | // watch mode constantly consumes 15-25% CPU due to polling. 46 | // https://github.com/evanw/esbuild/issues/1527 47 | const contexts = await Promise.all(Object.values(buildmap)); 48 | 49 | // Initial build 50 | await Promise.all(contexts.map((context) => context.rebuild())); 51 | 52 | const watchPaths = ["srcts/", "tailwind.config.js"]; 53 | 54 | const watcher = chokidar.watch(watchPaths, { 55 | ignored: ["**/node_modules/**", "**/dist/**", "**/*.d.ts"], 56 | persistent: true, 57 | ignoreInitial: true, 58 | }); 59 | 60 | let rebuildTimeout: NodeJS.Timeout; 61 | 62 | watcher.on("all", (eventName, path) => { 63 | console.log(`${eventName}: ${path}`); 64 | 65 | // Debounce rebuilds to avoid rapid successive builds 66 | clearTimeout(rebuildTimeout); 67 | rebuildTimeout = setTimeout(async () => { 68 | try { 69 | await Promise.all(contexts.map((context) => context.rebuild())); 70 | } catch (error) { 71 | console.error("Rebuild failed:", error); 72 | } 73 | }, 100); 74 | }); 75 | 76 | watcher.on("error", (error) => { 77 | console.error("Watcher error:", error); 78 | }); 79 | 80 | // Graceful shutdown 81 | process.on("SIGINT", async () => { 82 | console.log("\nShutting down..."); 83 | 84 | // Close file watcher 85 | await watcher.close(); 86 | 87 | // Dispose esbuild contexts 88 | await Promise.all(contexts.map((context) => context.dispose())); 89 | 90 | process.exit(0); 91 | }); 92 | } else { 93 | // Non-watch build 94 | Object.entries(buildmap).forEach(([target, build]) => 95 | build 96 | .then(async (context: esbuild.BuildContext) => { 97 | console.log(`Building .js bundle for ${target} target...`); 98 | await context.rebuild(); 99 | console.log( 100 | `✓ Successfully built ${target === "py" ? "py/www/main.js" : "r/www/main.js"}` 101 | ); 102 | await context.dispose(); 103 | }) 104 | .catch((e) => { 105 | console.error(`Build failed for ${target} target:`, e); 106 | process.exit(1); 107 | }) 108 | ); 109 | } 110 | } 111 | 112 | main().catch((e) => { 113 | console.error(e); 114 | process.exit(1); 115 | }); 116 | -------------------------------------------------------------------------------- /examples/2-inputs/srcts/BatchFormCard.tsx: -------------------------------------------------------------------------------- 1 | import { useShinyInput, useShinyOutput } from "@posit/shiny-react"; 2 | import React, { useState } from "react"; 3 | import InputOutputCard from "./InputOutputCard"; 4 | 5 | function BatchFormCard() { 6 | // Local state (NOT Shiny inputs) 7 | const [comment, setComment] = useState(""); 8 | const [priority, setPriority] = useState(50); 9 | const [features, setFeatures] = useState({ 10 | authentication: false, 11 | notifications: false, 12 | darkMode: false, 13 | analytics: false, 14 | }); 15 | 16 | // Shiny input/output for batch submission 17 | const [batchData, setBatchData] = useShinyInput( 18 | "batchdata", 19 | null, 20 | { 21 | // This makes it so there's no delay to sending the value when the button 22 | // is clicked. 23 | debounceMs: 0, 24 | // This makes it so that even if the input value is the same as the 25 | // previous, it will still cause invalidation of reactive functions on the 26 | // server. 27 | priority: "event", 28 | }, 29 | ); 30 | const [batchOutput, _] = useShinyOutput("batchout", ""); 31 | 32 | const handleFeatureChange = (feature: keyof typeof features) => { 33 | setFeatures((prev) => ({ 34 | ...prev, 35 | [feature]: !prev[feature], 36 | })); 37 | }; 38 | 39 | const handleSubmit = () => { 40 | const formData = { 41 | comment, 42 | priority, 43 | features, 44 | }; 45 | setBatchData(formData); 46 | }; 47 | 48 | const selectedFeaturesCount = Object.values(features).filter(Boolean).length; 49 | 50 | const inputElement = ( 51 |
52 |
53 | 54 |