diff --git a/docs/develop/standalone-activities-interactive-demo.mdx b/docs/develop/standalone-activities-interactive-demo.mdx new file mode 100644 index 0000000000..317c88acf9 --- /dev/null +++ b/docs/develop/standalone-activities-interactive-demo.mdx @@ -0,0 +1,383 @@ +--- +id: standalone-activities-interactive-demo +title: Standalone Activities Interactive Demo +sidebar_label: Standalone Activities (Interactive) +toc_max_heading_level: 3 +keywords: + - standalone activity + - activity execution + - execute activity + - activity handle + - quickstart + - interactive demo +tags: + - Activities + - Temporal Client + - Temporal SDKs +description: An interactive quickstart for Temporal Standalone Activities — run Activities directly from a Temporal Client without a Workflow. +--- + +:::tip SUPPORT, STABILITY, and DEPENDENCY INFO + +Temporal SDK support for [Standalone Activities](/standalone-activity) is at +[Pre-release](/evaluate/development-production-features/release-stages#pre-release). + +All APIs are experimental and may be subject to backwards-incompatible changes. + +::: + +Standalone Activities let you execute an Activity directly from a Temporal Client — no Workflow +required. The Activity is durably enqueued on the Temporal Server, executed by a Worker, and its +result is returned to the caller. + +Use the interactive demo below to explore the API, experiment with failure scenarios, and see the +generated SDK code and CLI command update in real time. + +import { StandaloneActivityDemo } from '@site/src/components'; + + + +--- + +## How it works + +When you call `client.ExecuteActivity()` (or the equivalent in your SDK), the following happens: + +1. **Connect** — Your application connects to the Temporal Server. +2. **Schedule** — The Server durably persists the Activity execution on the specified Task Queue. +3. **Poll** — A Worker polling that Task Queue picks up the Activity Task. +4. **Execute** — The Worker runs your Activity function with the provided arguments. +5. **Return** — The result is stored by the Server and returned to the original caller via the + `ActivityHandle`. + +Because the Server durably persists the Activity, it survives Worker restarts and network +interruptions. If the Activity fails, the Server automatically retries it according to the Retry +Policy you configure. + +### Standalone vs Workflow Activities + +| | Workflow Activity | Standalone Activity | +|---|---|---| +| Orchestrated by | A Workflow Definition | Your application code | +| Started with | `workflow.ExecuteActivity()` | `client.ExecuteActivity()` | +| Retry policy | Configured per `ActivityOptions` | Configured per `StartActivityOptions` | +| Visibility | Workflow Event History | Activity Visibility (List/Count) | +| Use case | Complex, multi-step orchestration | Simple, independent jobs | + +The Activity function and Worker registration are **identical** for both — only the execution path +differs. + +--- + +## Setup + +### 1. Install the Temporal CLI + +Download the Standalone Activity prerelease build of the Temporal CLI: + +```bash +# macOS (Homebrew) +brew install temporal + +# Or download directly from GitHub +# https://github.com/temporalio/cli/releases/tag/v1.6.2-standalone-activity +``` + +### 2. Start a local development server + +```bash +temporal server start-dev +``` + +The server starts at `localhost:7233` and the Web UI at +[http://localhost:8233](http://localhost:8233). Standalone Activities appear under the **Activities** +nav item in the Web UI. + +### 3. Install the SDK + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + +Requires **Go SDK v1.41.0** or higher. + +```bash +go get go.temporal.io/sdk@latest +``` + +Clone the samples repository to follow along: + +```bash +git clone https://github.com/temporalio/samples-go.git +cd samples-go +``` + + + + +Requires **Python SDK v1.23.0** or higher. + +```bash +uv add temporalio +# or: pip install temporalio +``` + +Clone the samples repository to follow along: + +```bash +git clone https://github.com/temporalio/samples-python.git +cd samples-python +``` + + + + +Requires **.NET SDK v1.12.0** or higher. + +```bash +dotnet add package Temporalio +``` + +Clone the samples repository to follow along: + +```bash +git clone https://github.com/temporalio/samples-dotnet.git +cd samples-dotnet +``` + + + + +### 4. Run the Worker + +The Worker registers the Activity and polls the Task Queue. Start it in a dedicated terminal: + + + + +```bash +go run standalone-activity/helloworld/worker/main.go +``` + + + + +```bash +uv run hello_standalone_activity/worker.py +``` + + + + +```bash +dotnet run --project src/StandaloneActivity worker +``` + + + + +### 5. Execute the Activity + +In a separate terminal, run the starter: + + + + +```bash +go run standalone-activity/helloworld/starter/main.go +``` + + + + +```bash +uv run hello_standalone_activity/execute_activity.py +``` + + + + +```bash +dotnet run --project src/StandaloneActivity execute-activity +``` + + + + +Or use the Temporal CLI directly: + +```bash +temporal activity execute \ + --type Activity \ + --activity-id my-activity-id \ + --task-queue my-task-queue \ + --start-to-close-timeout 10s \ + --input '"World"' +``` + +--- + +## Key API concepts + +### `ExecuteActivity` — start and wait + +The primary call. Durably enqueues the Activity, waits for execution, and returns the result: + + + + +```go +handle, err := c.ExecuteActivity(ctx, client.StartActivityOptions{ + ID: "my-activity-id", + TaskQueue: "my-task-queue", + StartToCloseTimeout: 10 * time.Second, +}, helloworld.Activity, "World") + +var result string +err = handle.Get(ctx, &result) +``` + + + + +```python +result = await client.execute_activity( + compose_greeting, + args=[ComposeGreetingInput("Hello", "World")], + id="my-activity-id", + task_queue="my-task-queue", + start_to_close_timeout=timedelta(seconds=10), +) +``` + + + + +```csharp +var result = await client.ExecuteActivityAsync( + () => MyActivities.ComposeGreetingAsync( + new ComposeGreetingInput("Hello", "World")), + new("my-activity-id", "my-task-queue") + { + StartToCloseTimeout = TimeSpan.FromSeconds(10), + }); +``` + + + + +### `StartActivity` — fire and forget + +Enqueues the Activity without waiting for it to finish. Useful when you want to kick off +long-running work and check back later: + + + + +```go +handle, err := c.ExecuteActivity(ctx, options, helloworld.Activity, "World") +// handle.Get() can be called later +``` + + + + +```python +handle = await client.start_activity( + compose_greeting, + args=[ComposeGreetingInput("Hello", "World")], + id="my-activity-id", + task_queue="my-task-queue", + start_to_close_timeout=timedelta(seconds=10), +) +# Later: +result = await handle.result() +``` + + + + +```csharp +var handle = await client.StartActivityAsync( + () => MyActivities.ComposeGreetingAsync( + new ComposeGreetingInput("Hello", "World")), + new("my-activity-id", "my-task-queue") + { + StartToCloseTimeout = TimeSpan.FromSeconds(10), + }); +// Later: +var result = await handle.GetResultAsync(); +``` + + + + +### List and Count Activities + + + + +```go +// List +resp, err := c.ListActivities(ctx, client.ListActivitiesOptions{ + Query: "TaskQueue = 'my-task-queue'", +}) +for info, err := range resp.Results { ... } + +// Count +resp, err := c.CountActivities(ctx, client.CountActivitiesOptions{ + Query: "TaskQueue = 'my-task-queue'", +}) +log.Println("Total:", resp.Count) +``` + + + + +```python +# List +async for info in client.list_activities( + query="TaskQueue = 'my-task-queue'" +): + print(info.activity_id, info.status) + +# Count +resp = await client.count_activities( + query="TaskQueue = 'my-task-queue'" +) +print("Total:", resp.count) +``` + + + + +```csharp +// List +await foreach (var info in client.ListActivitiesAsync( + "TaskQueue = 'my-task-queue'")) +{ + Console.WriteLine($"{info.ActivityId}: {info.Status}"); +} + +// Count +var resp = await client.CountActivitiesAsync( + "TaskQueue = 'my-task-queue'"); +Console.WriteLine($"Total: {resp.Count}"); +``` + + + + +The `Query` parameter uses the same [List Filter](/list-filter) syntax as Workflow Visibility. + +--- + +## Next steps + +For complete API reference and advanced usage, see the SDK-specific guides: + +- [Standalone Activities — Go SDK](/develop/go/standalone-activities) +- [Standalone Activities — Python SDK](/develop/python/standalone-activities) +- [Standalone Activities — .NET SDK](/develop/dotnet/standalone-activities) diff --git a/src/components/elements/StandaloneActivityDemo.js b/src/components/elements/StandaloneActivityDemo.js new file mode 100644 index 0000000000..91625c5952 --- /dev/null +++ b/src/components/elements/StandaloneActivityDemo.js @@ -0,0 +1,602 @@ +import CodeBlock from '@theme/CodeBlock'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import styles from './standalone-activity-demo.module.css'; + +// --------------------------------------------------------------------------- +// Code generation +// --------------------------------------------------------------------------- + +function generateSdkCode(language, config) { + const { activityId, taskQueue, greeting, name, timeout, timeoutType, simulateFailures, maxRetries } = config; + const expectedResult = `${greeting}, ${name}!`; + + if (language === 'go') { + const timeoutField = + timeoutType === 'start_to_close' + ? `StartToCloseTimeout: ${timeout} * time.Second,` + : `ScheduleToCloseTimeout: ${timeout} * time.Second,`; + const retryPolicy = + simulateFailures && maxRetries > 0 + ? `\n\tRetryPolicy: &temporal.RetryPolicy{\n\t\tMaximumAttempts: ${maxRetries + 1},\n\t},` + : ''; + return `activityOptions := client.StartActivityOptions{ +\tID: "${activityId}", +\tTaskQueue: "${taskQueue}", +\t${timeoutField}${retryPolicy} +} + +handle, err := c.ExecuteActivity(ctx, activityOptions, +\thelloworld.Activity, "${greeting}", "${name}") +if err != nil { +\tlog.Fatalln("Unable to execute activity", err) +} + +var result string +err = handle.Get(ctx, &result) +// result: "${expectedResult}"`; + } + + if (language === 'python') { + const timeoutField = + timeoutType === 'start_to_close' + ? `start_to_close_timeout=timedelta(seconds=${timeout}),` + : `schedule_to_close_timeout=timedelta(seconds=${timeout}),`; + const retryPolicy = + simulateFailures && maxRetries > 0 + ? `\n retry_policy=RetryPolicy(\n maximum_attempts=${maxRetries + 1},\n ),` + : ''; + return `result = await client.execute_activity( + compose_greeting, + args=[ComposeGreetingInput("${greeting}", "${name}")], + id="${activityId}", + task_queue="${taskQueue}", + ${timeoutField}${retryPolicy} +) +# result: "${expectedResult}"`; + } + + if (language === 'dotnet') { + const timeoutField = + timeoutType === 'start_to_close' + ? `StartToCloseTimeout = TimeSpan.FromSeconds(${timeout}),` + : `ScheduleToCloseTimeout = TimeSpan.FromSeconds(${timeout}),`; + const retryPolicy = + simulateFailures && maxRetries > 0 + ? `\n MaximumAttempts = ${maxRetries + 1},` + : ''; + return `var result = await client.ExecuteActivityAsync( + () => MyActivities.ComposeGreetingAsync( + new ComposeGreetingInput("${greeting}", "${name}")), + new("${activityId}", "${taskQueue}") + { + ${timeoutField}${retryPolicy} + }); +// result: "${expectedResult}"`; + } + + return ''; +} + +function generateCliCode(config) { + const { activityId, taskQueue, name, timeout, timeoutType } = config; + const timeoutFlag = + timeoutType === 'start_to_close' + ? `--start-to-close-timeout ${timeout}s` + : `--schedule-to-close-timeout ${timeout}s`; + return `temporal activity execute \\ + --type Activity \\ + --activity-id ${activityId} \\ + --task-queue ${taskQueue} \\ + ${timeoutFlag} \\ + --input '"${name}"'`; +} + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const LANGUAGES = [ + { id: 'go', label: 'Go' }, + { id: 'python', label: 'Python' }, + { id: 'dotnet', label: '.NET' }, +]; + +const FLOW_NODES = [ + { label: 'Client', sub: 'Your App' }, + { label: 'Server', sub: 'Temporal' }, + { label: 'Task Queue', sub: '' }, + { label: 'Worker', sub: '' }, + { label: 'Activity', sub: 'Function' }, +]; + +const IDLE_NODES = ['pending', 'pending', 'pending', 'pending', 'pending']; + +const DEFAULT_CONFIG = { + activityId: 'my-activity-id', + taskQueue: 'my-task-queue', + greeting: 'Hello', + name: 'World', + timeout: 10, + timeoutType: 'start_to_close', + simulateFailures: false, + failCount: 1, + maxRetries: 2, +}; + +// --------------------------------------------------------------------------- +// Main component +// --------------------------------------------------------------------------- + +export default function StandaloneActivityDemo() { + const [language, setLanguage] = useState('go'); + const [config, setConfig] = useState({ ...DEFAULT_CONFIG }); + + const [sim, setSim] = useState({ + running: false, + nodeStates: [...IDLE_NODES], + log: [], + status: 'idle', // 'idle' | 'running' | 'completed' | 'failed' + result: null, + }); + + const [history, setHistory] = useState([]); + + const runIdRef = useRef(0); + const logScrollRef = useRef(null); + + // Auto-scroll log to bottom + useEffect(() => { + if (logScrollRef.current) { + logScrollRef.current.scrollTop = logScrollRef.current.scrollHeight; + } + }, [sim.log]); + + const updateConfig = useCallback((key, value) => { + setConfig((prev) => ({ ...prev, [key]: value })); + }, []); + + const handleExecute = useCallback(() => { + const runId = ++runIdRef.current; + const isCancelled = () => runIdRef.current !== runId; + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + + const startTime = Date.now(); + const logEntries = []; + + const elapsed = () => ((Date.now() - startTime) / 1000).toFixed(2); + + /** Push a log entry and update React state atomically with nodeStates */ + const update = (nodeStates, msg, type = 'info') => { + if (isCancelled()) return; + if (msg) logEntries.push({ time: elapsed(), msg, type }); + setSim((prev) => ({ + ...prev, + running: true, + nodeStates, + log: [...logEntries], + status: 'running', + })); + }; + + /** Push a log-only entry (no node state change) */ + const addLog = (msg, type = 'info') => { + if (isCancelled()) return; + logEntries.push({ time: elapsed(), msg, type }); + setSim((prev) => ({ ...prev, log: [...logEntries] })); + }; + + // Reset UI immediately + setSim({ + running: true, + nodeStates: [...IDLE_NODES], + log: [], + status: 'running', + result: null, + }); + + (async () => { + let attempt = 1; + + // ── Step 0: connect ────────────────────────────────────────────────── + update( + ['active', 'pending', 'pending', 'pending', 'pending'], + 'Connecting to Temporal Server at localhost:7233...' + ); + await sleep(400); + if (isCancelled()) return; + + // ── Step 1: schedule ───────────────────────────────────────────────── + update( + ['completed', 'active', 'pending', 'pending', 'pending'], + `Scheduling activity "${config.activityId}" on task queue "${config.taskQueue}"...` + ); + await sleep(600); + if (isCancelled()) return; + + update(['completed', 'completed', 'pending', 'pending', 'pending'], null); + + // ── Retry loop ─────────────────────────────────────────────────────── + while (true) { + if (isCancelled()) return; + + // Step 2: worker poll + update( + ['completed', 'completed', 'active', 'pending', 'pending'], + attempt === 1 + ? 'Worker polling task queue for activity tasks...' + : `[Retry ${attempt - 1}] Worker polling task queue...` + ); + await sleep(550); + if (isCancelled()) return; + + // Step 3: execute + update( + ['completed', 'completed', 'completed', 'active', 'pending'], + `[Attempt ${attempt}] Executing Activity("${config.greeting}", "${config.name}")...` + ); + await sleep(750); + if (isCancelled()) return; + + const shouldFail = config.simulateFailures && attempt <= config.failCount; + const maxAttempts = config.maxRetries + 1; + + if (shouldFail) { + const retriesLeft = maxAttempts - attempt; + update( + ['completed', 'completed', 'pending', 'failed', 'pending'], + `[Attempt ${attempt}/${maxAttempts}] Activity failed: ApplicationError`, + 'error' + ); + await sleep(350); + if (isCancelled()) return; + + if (retriesLeft <= 0) { + logEntries.push({ + time: elapsed(), + msg: `Activity exhausted all ${maxAttempts} attempt(s). No more retries.`, + type: 'error', + }); + setSim((prev) => ({ + ...prev, + running: false, + status: 'failed', + log: [...logEntries], + })); + setHistory((prev) => + [ + { + activityId: config.activityId, + status: 'Failed', + duration: `${((Date.now() - startTime) / 1000).toFixed(1)}s`, + attempts: attempt, + result: '—', + timestamp: new Date().toLocaleTimeString(), + }, + ...prev, + ].slice(0, 10) + ); + return; + } + + addLog( + `Scheduling retry in 1s… (${retriesLeft} retry attempt${retriesLeft > 1 ? 's' : ''} remaining)`, + 'warn' + ); + await sleep(1000); + if (isCancelled()) return; + + attempt++; + continue; // jump back to worker poll + } + + // ── Success ─────────────────────────────────────────────────────── + update( + ['completed', 'completed', 'completed', 'completed', 'active'], + `[Attempt ${attempt}] Activity function returned successfully!`, + 'success' + ); + await sleep(400); + if (isCancelled()) return; + + const result = `${config.greeting}, ${config.name}!`; + logEntries.push({ time: elapsed(), msg: `Result: "${result}"`, type: 'success' }); + + setSim({ + running: false, + nodeStates: ['completed', 'completed', 'completed', 'completed', 'completed'], + log: [...logEntries], + status: 'completed', + result, + }); + + setHistory((prev) => + [ + { + activityId: config.activityId, + status: 'Completed', + duration: `${((Date.now() - startTime) / 1000).toFixed(1)}s`, + attempts: attempt, + result, + timestamp: new Date().toLocaleTimeString(), + }, + ...prev, + ].slice(0, 10) + ); + return; + } + })(); + }, [config]); + + const codeLanguage = language === 'dotnet' ? 'csharp' : language; + + const failureNote = + config.simulateFailures + ? config.failCount > config.maxRetries + ? `⚠ All ${config.maxRetries + 1} attempt(s) will fail — failCount exceeds maxRetries.` + : `ℹ Activity will fail ${config.failCount} time(s), then succeed on attempt ${config.failCount + 1}.` + : null; + + return ( +
+ {/* Language tabs */} +
+ {LANGUAGES.map(({ id, label }) => ( + + ))} +
+ +
+ {/* ── Left column: configure + code ── */} +
+
+

Configure Activity

+
+ updateConfig('activityId', v)} + /> + updateConfig('taskQueue', v)} + /> + updateConfig('greeting', v)} + /> + updateConfig('name', v)} + /> + updateConfig('timeout', Number(v))} + /> +
+ + +
+
+
+ +
+

Failure Simulation

+
+
+ +
+ {config.simulateFailures && ( + <> + updateConfig('failCount', Number(v))} + /> + updateConfig('maxRetries', Number(v))} + /> + + )} +
+ {failureNote &&

{failureNote}

} +
+ +
+

SDK Code

+ {generateSdkCode(language, config)} +
+ +
+

CLI Command

+ {generateCliCode(config)} +
+
+ + {/* ── Right column: simulation ── */} +
+
+ +
+ +
+

Execution Flow

+
+ {FLOW_NODES.map((node, i) => ( + +
+
{node.label}
+ {node.sub && ( +
{node.sub}
+ )} +
+ {i < FLOW_NODES.length - 1 && ( +
+ › +
+ )} +
+ ))} +
+
+ +
+

Activity Log

+
+ {sim.log.length === 0 ? ( +
+ Click "Execute Activity" to run the simulation +
+ ) : ( + sim.log.map((entry, i) => ( +
+ [{entry.time}s] + {entry.msg} +
+ )) + )} +
+ + {sim.status === 'completed' && sim.result && ( +
+ ✅ Result: "{sim.result}" +
+ )} + {sim.status === 'failed' && ( +
+ ❌ Activity failed after exhausting all retry attempts +
+ )} +
+ + {history.length > 0 && ( +
+

+ Activity History{' '} + ({history.length}) +

+
+ + + + + + + + + + + + {history.map((h, i) => ( + + + + + + + + ))} + +
Activity IDStatusDurationAttemptsResult
{h.activityId} + + {h.status} + + {h.duration}{h.attempts}{h.result}
+
+
+ )} +
+
+
+ ); +} + +// --------------------------------------------------------------------------- +// Sub-components +// --------------------------------------------------------------------------- + +function ConfigField({ label, value, onChange, type = 'text', min, max }) { + return ( +
+ + onChange(e.target.value)} + /> +
+ ); +} diff --git a/src/components/elements/standalone-activity-demo.module.css b/src/components/elements/standalone-activity-demo.module.css new file mode 100644 index 0000000000..4b4db563d7 --- /dev/null +++ b/src/components/elements/standalone-activity-demo.module.css @@ -0,0 +1,416 @@ +/* ── Root ───────────────────────────────────────────────────────────────── */ + +.demo { + font-family: var(--ifm-font-family-base); +} + +/* ── Language tabs ──────────────────────────────────────────────────────── */ + +.languageTabs { + display: flex; + gap: 6px; + margin-bottom: 16px; + border-bottom: 2px solid var(--ifm-color-emphasis-200); + padding-bottom: 10px; +} + +.langTab { + padding: 5px 18px; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 4px; + background: var(--ifm-background-color); + color: var(--ifm-font-color-base); + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + transition: background 0.15s, color 0.15s, border-color 0.15s; +} + +.langTab:hover { + background: var(--ifm-color-emphasis-100); +} + +.langTabActive { + background: var(--ifm-color-primary); + color: #fff; + border-color: var(--ifm-color-primary); +} + +/* ── Two-column layout ──────────────────────────────────────────────────── */ + +.columns { + display: flex; + gap: 24px; + align-items: flex-start; +} + +.leftCol, +.rightCol { + flex: 1; + min-width: 0; +} + +@media (max-width: 900px) { + .columns { + flex-direction: column; + } +} + +/* ── Section ────────────────────────────────────────────────────────────── */ + +.section { + margin-bottom: 20px; +} + +.sectionTitle { + font-size: 0.95rem; + font-weight: 600; + margin: 0 0 8px; + color: var(--ifm-font-color-base); +} + +/* ── Config form ────────────────────────────────────────────────────────── */ + +.configGrid { + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 6px; + overflow: hidden; +} + +.configRow { + display: flex; + align-items: center; + padding: 7px 12px; + border-bottom: 1px solid var(--ifm-color-emphasis-200); + background: var(--ifm-background-surface-color); +} + +.configRow:last-child { + border-bottom: none; +} + +.configLabel { + flex: 1; + font-size: 0.83rem; + color: var(--ifm-font-color-secondary); + user-select: none; +} + +.configInput, +.configSelect { + width: 150px; + padding: 4px 8px; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 4px; + background: var(--ifm-background-color); + color: var(--ifm-font-color-base); + font-size: 0.83rem; +} + +.checkbox { + margin-right: 6px; + cursor: pointer; +} + +.simNote { + font-size: 0.8rem; + color: var(--ifm-font-color-secondary); + margin: 8px 0 0; + padding: 8px 12px; + background: var(--ifm-color-emphasis-100); + border-radius: 4px; + line-height: 1.4; +} + +/* ── Execute button ─────────────────────────────────────────────────────── */ + +.executeBtn { + width: 100%; + padding: 12px 24px; + background: var(--ifm-color-primary); + color: #fff; + border: none; + border-radius: 6px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: background 0.2s; +} + +.executeBtn:hover:not(.executeBtnDisabled) { + background: var(--ifm-color-primary-dark); +} + +.executeBtnDisabled { + opacity: 0.65; + cursor: not-allowed; +} + +/* ── Spinner ────────────────────────────────────────────────────────────── */ + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.spinner { + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid rgba(255, 255, 255, 0.35); + border-top-color: #fff; + border-radius: 50%; + animation: spin 0.75s linear infinite; + flex-shrink: 0; +} + +/* ── Flow diagram ───────────────────────────────────────────────────────── */ + +.flowDiagram { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 12px; + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 6px; + overflow-x: auto; + gap: 4px; +} + +.flowNode { + display: flex; + flex-direction: column; + align-items: center; + padding: 8px 10px; + border-radius: 6px; + border: 2px solid var(--ifm-color-emphasis-300); + background: var(--ifm-background-color); + min-width: 68px; + text-align: center; + transition: border-color 0.3s, background 0.3s, opacity 0.3s; +} + +.flowNodeLabel { + font-size: 0.75rem; + font-weight: 600; + white-space: nowrap; + transition: color 0.3s; +} + +.flowNodeSub { + font-size: 0.63rem; + color: var(--ifm-font-color-secondary); + margin-top: 2px; + white-space: nowrap; +} + +/* Node states */ + +.flowNode_pending { + opacity: 0.38; +} + +@keyframes nodePulse { + 0%, + 100% { + box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.45); + } + 50% { + box-shadow: 0 0 0 7px rgba(59, 130, 246, 0); + } +} + +.flowNode_active { + border-color: #3b82f6; + background: rgba(59, 130, 246, 0.08); + animation: nodePulse 1.4s ease-in-out infinite; +} + +.flowNode_active .flowNodeLabel { + color: #3b82f6; +} + +.flowNode_completed { + border-color: #22c55e; + background: rgba(34, 197, 94, 0.08); +} + +.flowNode_completed .flowNodeLabel { + color: #15803d; +} + +.flowNode_failed { + border-color: #ef4444; + background: rgba(239, 68, 68, 0.08); +} + +.flowNode_failed .flowNodeLabel { + color: #dc2626; +} + +/* Arrows */ + +.flowArrow { + font-size: 1.6rem; + color: var(--ifm-color-emphasis-300); + flex-shrink: 0; + line-height: 1; + transition: color 0.3s; + user-select: none; +} + +.flowArrowLit { + color: #22c55e; +} + +/* ── Activity log ───────────────────────────────────────────────────────── */ + +.log { + height: 190px; + overflow-y: auto; + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 6px; + padding: 10px 12px; + font-family: var(--ifm-font-family-monospace); + font-size: 0.78rem; + line-height: 1.55; +} + +.logPlaceholder { + color: var(--ifm-font-color-secondary); + font-style: italic; + padding: 4px 0; +} + +.logLine { + display: flex; + gap: 8px; + padding: 1px 0; +} + +.logTime { + color: var(--ifm-font-color-secondary); + flex-shrink: 0; +} + +.logLine_info .logMsg { + color: var(--ifm-font-color-base); +} + +.logLine_success .logMsg { + color: #15803d; + font-weight: 600; +} + +.logLine_error .logMsg { + color: #dc2626; +} + +.logLine_warn .logMsg { + color: #b45309; +} + +/* Result banners */ + +.resultSuccess { + margin-top: 8px; + padding: 9px 14px; + background: rgba(34, 197, 94, 0.1); + border: 1px solid rgba(34, 197, 94, 0.5); + border-radius: 6px; + color: #15803d; + font-size: 0.88rem; +} + +.resultFailed { + margin-top: 8px; + padding: 9px 14px; + background: rgba(239, 68, 68, 0.08); + border: 1px solid rgba(239, 68, 68, 0.45); + border-radius: 6px; + color: #dc2626; + font-size: 0.88rem; +} + +/* ── Activity history table ─────────────────────────────────────────────── */ + +.historyCount { + font-weight: 400; + color: var(--ifm-font-color-secondary); + font-size: 0.85rem; +} + +.historyWrapper { + overflow-x: auto; + border: 1px solid var(--ifm-color-emphasis-200); + border-radius: 6px; +} + +.historyTable { + width: 100%; + border-collapse: collapse; + font-size: 0.82rem; +} + +.historyTable th { + padding: 7px 12px; + text-align: left; + background: var(--ifm-background-surface-color); + border-bottom: 1px solid var(--ifm-color-emphasis-200); + font-weight: 600; + color: var(--ifm-font-color-secondary); + font-size: 0.72rem; + text-transform: uppercase; + letter-spacing: 0.05em; + white-space: nowrap; +} + +.historyTable td { + padding: 7px 12px; + border-bottom: 1px solid var(--ifm-color-emphasis-100); + vertical-align: middle; +} + +.historyTable tr:last-child td { + border-bottom: none; +} + +.historyId { + font-family: var(--ifm-font-family-monospace); + font-size: 0.78rem; +} + +.historyResult { + font-family: var(--ifm-font-family-monospace); + font-size: 0.78rem; + color: var(--ifm-font-color-secondary); +} + +.badgeSuccess { + display: inline-block; + padding: 2px 8px; + background: rgba(34, 197, 94, 0.12); + color: #15803d; + border-radius: 9999px; + font-size: 0.74rem; + font-weight: 600; + white-space: nowrap; +} + +.badgeFailed { + display: inline-block; + padding: 2px 8px; + background: rgba(239, 68, 68, 0.1); + color: #dc2626; + border-radius: 9999px; + font-size: 0.74rem; + font-weight: 600; + white-space: nowrap; +} diff --git a/src/components/index.js b/src/components/index.js index 07b4ad9ef6..df840f56ae 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -1,5 +1,6 @@ // Website components export { default as RetrySimulator } from './elements/RetrySimulator'; +export { default as StandaloneActivityDemo } from './elements/StandaloneActivityDemo'; export { default as HomePageHero } from './elements/HomePageHero'; export { SdkLogos } from './elements/SdkLogos'; export { SdkLogosAsBlocks } from './elements/SdkLogosAsBlocks';