diff --git a/.github/workflows/validate-assets.yml b/.github/workflows/validate-assets.yml
index 17ac87e..4d58717 100644
--- a/.github/workflows/validate-assets.yml
+++ b/.github/workflows/validate-assets.yml
@@ -21,18 +21,17 @@ jobs:
with:
python-version: "3.12"
+ - name: Install uv
+ run: pip install uv
+
- name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install uv
- uv pip install --system -e ".[dev]"
+ run: uv sync --group dev
- name: Generate markdown files
- run: python scripts/generate_markdown.py
+ run: uv run python scripts/generate_markdown.py
- name: Check for uncommitted changes
- run: |
- git diff --exit-code assets/docs
+ run: git diff --exit-code assets/docs
- name: Run asset verification tests
- run: pytest tests/test_generated_assets.py
+ run: uv run pytest tests/test_generated_assets.py
diff --git a/app/registry/components.py b/app/registry/components.py
index ab432ea..e842486 100644
--- a/app/registry/components.py
+++ b/app/registry/components.py
@@ -105,6 +105,10 @@
"files": ["components/ui/link.py"],
"dependencies": ["hugeicon", "twmerge"],
},
+ "marker": {
+ "files": ["components/ui/marker.py"],
+ "dependencies": ["twmerge"],
+ },
"menu": {
"files": ["components/ui/menu.py"],
"dependencies": ["hugeicon", "others_icons", "twmerge", "base_ui", "button"],
@@ -133,6 +137,10 @@
"files": ["components/ui/slider.py"],
"dependencies": ["base_ui"],
},
+ "spinner": {
+ "files": ["components/ui/spinner.py"],
+ "dependencies": ["twmerge"],
+ },
"switch": {
"files": ["components/ui/switch.py"],
"dependencies": ["base_ui"],
diff --git a/app/templates/toc.py b/app/templates/toc.py
index 6d7c4aa..5eb06be 100644
--- a/app/templates/toc.py
+++ b/app/templates/toc.py
@@ -199,7 +199,14 @@ def table_of_content(url: str, toc_data: List[Dict]):
"On This Page",
class_name="text-xs text-muted-foreground font-medium pb-2",
),
- _create_markdown_toc_links(url, toc_data),
+ rx.el.div(
+ _create_markdown_toc_links(url, toc_data),
+ rx.el.div(class_name="py-2"),
+ class_name=(
+ "w-full flex flex-col "
+ "h-[calc(100svh-22rem)] overflow-y-auto scrollbar-none scroll-fade scroll-fade-4"
+ ),
+ ),
class_name="w-full flex flex-col",
),
class_name="flex flex-col w-full h-full p-4 gap-y-6",
diff --git a/app/www/anatomy.py b/app/www/anatomy.py
index 058193f..569aa8a 100644
--- a/app/www/anatomy.py
+++ b/app/www/anatomy.py
@@ -120,6 +120,12 @@
kbd(),
)""",
"link": """link()""",
+ "marker": """marker.root(
+ marker.icon(),
+ marker.content(),
+)
+""",
+ "spinner": """spinner()""",
"menu": """menu.root(
menu.trigger(),
menu.portal(
diff --git a/app/www/library/examples/marker_border.py b/app/www/library/examples/marker_border.py
new file mode 100644
index 0000000..ece8664
--- /dev/null
+++ b/app/www/library/examples/marker_border.py
@@ -0,0 +1,25 @@
+import reflex as rx
+
+from components.icons.hugeicon import hi
+from components.ui.marker import marker
+
+
+def marker_border():
+ return rx.el.div(
+ marker.root(
+ marker.icon(hi("GitBranchIcon")),
+ marker.content("Switched to release-candidate"),
+ variant="border",
+ ),
+ marker.root(
+ marker.icon(hi("Search01Icon")),
+ marker.content("Reviewed 8 related files"),
+ variant="border",
+ ),
+ marker.root(
+ marker.icon(hi("File01Icon")),
+ marker.content("Opened implementation notes"),
+ variant="border",
+ ),
+ class_name="flex w-full max-w-sm flex-col gap-3 py-12",
+ )
diff --git a/app/www/library/examples/marker_links_and_buttons.py b/app/www/library/examples/marker_links_and_buttons.py
new file mode 100644
index 0000000..7ea4443
--- /dev/null
+++ b/app/www/library/examples/marker_links_and_buttons.py
@@ -0,0 +1,35 @@
+import reflex as rx
+
+from components.icons.hugeicon import hi
+from components.ui.marker import marker
+
+
+def marker_link_button():
+ return rx.el.div(
+ marker.root(
+ rx.el.a(
+ marker.icon(hi("GitBranchIcon")),
+ marker.content("View the pull request"),
+ href="#links-and-buttons",
+ class_name="group flex flex-row items-center gap-x-2 underline transition-colors hover:text-foreground",
+ ),
+ variant="default",
+ ),
+ marker.root(
+ rx.el.button(
+ marker.icon(
+ hi("ArrowMoveUpRightIcon"),
+ class_name="group-hover:text-foreground transition-colors",
+ ),
+ marker.content(
+ "Revert this change",
+ class_name="group-hover:text-foreground transition-colors",
+ ),
+ type="button",
+ class_name="group flex flex-row items-center gap-x-2 transition-colors",
+ on_click=rx.toast("You clicked the revert button"),
+ ),
+ variant="default",
+ ),
+ class_name="flex w-full max-w-sm flex-col gap-8 py-12 justify-center",
+ )
diff --git a/app/www/library/examples/marker_separator.py b/app/www/library/examples/marker_separator.py
new file mode 100644
index 0000000..ed16b80
--- /dev/null
+++ b/app/www/library/examples/marker_separator.py
@@ -0,0 +1,21 @@
+import reflex as rx
+
+from components.ui.marker import marker
+
+
+def marker_separator():
+ return rx.el.div(
+ marker.root(
+ marker.content("Today"),
+ variant="separator",
+ ),
+ marker.root(
+ marker.content("Worked for 42s"),
+ variant="separator",
+ ),
+ marker.root(
+ marker.content("Conversation compacted"),
+ variant="separator",
+ ),
+ class_name="flex w-full max-w-sm flex-col gap-8 py-12",
+ )
diff --git a/app/www/library/examples/marker_shimmer.py b/app/www/library/examples/marker_shimmer.py
new file mode 100644
index 0000000..2947d2d
--- /dev/null
+++ b/app/www/library/examples/marker_shimmer.py
@@ -0,0 +1,18 @@
+import reflex as rx
+
+from components.ui.marker import marker
+
+
+def marker_shimmer():
+ return rx.el.div(
+ marker.root(
+ marker.content("Thinking...", class_name="shimmer"),
+ role="status",
+ ),
+ marker.root(
+ marker.content("Reading 4 files", class_name="shimmer"),
+ variant="separator",
+ role="status",
+ ),
+ class_name="flex w-full max-w-sm flex-col gap-8 py-12",
+ )
diff --git a/app/www/library/examples/marker_status.py b/app/www/library/examples/marker_status.py
new file mode 100644
index 0000000..b52d2b7
--- /dev/null
+++ b/app/www/library/examples/marker_status.py
@@ -0,0 +1,21 @@
+import reflex as rx
+
+from components.ui.marker import marker
+from components.ui.spinner import spinner
+
+
+def marker_status_demo():
+ return rx.el.div(
+ marker.root(
+ marker.icon(spinner()),
+ marker.content("Compacting conversation"),
+ role="status",
+ ),
+ marker.root(
+ marker.icon(spinner()),
+ marker.content("Running tests"),
+ variant="separator",
+ role="status",
+ ),
+ class_name="flex w-full max-w-sm flex-col gap-8 py-12",
+ )
diff --git a/app/www/library/examples/marker_variants.py b/app/www/library/examples/marker_variants.py
new file mode 100644
index 0000000..4128f9b
--- /dev/null
+++ b/app/www/library/examples/marker_variants.py
@@ -0,0 +1,23 @@
+import reflex as rx
+
+from components.ui.marker import marker
+
+
+def marker_variants_demo():
+ return rx.el.div(
+ # Default Marker
+ marker.root(
+ marker.content("A default marker for inline notes."),
+ ),
+ # Separator Marker
+ marker.root(
+ marker.content("A separator marker"),
+ variant="separator",
+ ),
+ # Border Marker
+ marker.root(
+ marker.content("A border marker for row boundaries."),
+ variant="border",
+ ),
+ class_name="flex w-full max-w-sm flex-col gap-8 py-12",
+ )
diff --git a/app/www/library/examples/marker_with_icon.py b/app/www/library/examples/marker_with_icon.py
new file mode 100644
index 0000000..854eb35
--- /dev/null
+++ b/app/www/library/examples/marker_with_icon.py
@@ -0,0 +1,24 @@
+import reflex as rx
+
+from components.icons.hugeicon import hi
+from components.ui.marker import marker
+
+
+def marker_with_icon():
+ return rx.el.div(
+ marker.root(
+ marker.icon(hi("GitBranchIcon")),
+ marker.content("Switched to a new branch"),
+ ),
+ marker.root(
+ marker.icon(hi("Search01Icon")),
+ marker.content("Explored 4 files"),
+ variant="separator",
+ ),
+ marker.root(
+ marker.icon(hi("BookOpenCheckIcon")),
+ marker.content("Syncing completed"),
+ class_name="flex-col",
+ ),
+ class_name="flex w-full max-w-sm flex-col gap-12 py-12",
+ )
diff --git a/app/www/library/examples/spinner_badge.py b/app/www/library/examples/spinner_badge.py
new file mode 100644
index 0000000..2f0131b
--- /dev/null
+++ b/app/www/library/examples/spinner_badge.py
@@ -0,0 +1,13 @@
+import reflex as rx
+
+from components.ui.badge import badge
+from components.ui.spinner import spinner
+
+
+def spinner_badge():
+ return rx.el.div(
+ badge(spinner(), "Syncing"),
+ badge(spinner(), "Updating", variant="secondary"),
+ badge(spinner(), "Processing", variant="outline"),
+ class_name="flex items-center gap-4",
+ )
diff --git a/app/www/library/examples/spinner_button.py b/app/www/library/examples/spinner_button.py
new file mode 100644
index 0000000..aebf7ad
--- /dev/null
+++ b/app/www/library/examples/spinner_button.py
@@ -0,0 +1,13 @@
+import reflex as rx
+
+from components.ui.button import button
+from components.ui.spinner import spinner
+
+
+def spinner_button():
+ return rx.el.div(
+ button(spinner(), "Loading...", disabled=True, size="sm"),
+ button(spinner(), "Please wait", disabled=True, size="sm", variant="outline"),
+ button(spinner(), "Processing", disabled=True, size="sm", variant="secondary"),
+ class_name="flex flex-col items-center gap-4",
+ )
diff --git a/app/www/library/examples/spinner_marker.py b/app/www/library/examples/spinner_marker.py
new file mode 100644
index 0000000..7b44992
--- /dev/null
+++ b/app/www/library/examples/spinner_marker.py
@@ -0,0 +1,27 @@
+import reflex as rx
+
+from components.ui.marker import marker
+from components.ui.spinner import spinner
+
+
+def spinner_marker():
+ return rx.el.div(
+ marker.root(
+ marker.icon(spinner()),
+ marker.content("Thinking…", class_name="shimmer w-fit"),
+ role="status",
+ ),
+ marker.root(
+ marker.icon(spinner()),
+ marker.content("Generating response…", class_name="shimmer w-fit"),
+ variant="border",
+ role="status",
+ ),
+ marker.root(
+ marker.icon(spinner()),
+ marker.content("Processing"),
+ variant="separator",
+ role="status",
+ ),
+ class_name="flex w-full max-w-sm flex-col gap-6",
+ )
diff --git a/app/www/library/examples/spinner_size.py b/app/www/library/examples/spinner_size.py
new file mode 100644
index 0000000..1df14b6
--- /dev/null
+++ b/app/www/library/examples/spinner_size.py
@@ -0,0 +1,13 @@
+import reflex as rx
+
+from components.ui.spinner import spinner
+
+
+def spinner_size():
+ return rx.el.div(
+ spinner(class_name="size-3"),
+ spinner(class_name="size-4"),
+ spinner(class_name="size-6"),
+ spinner(class_name="size-8"),
+ class_name="flex items-center gap-6",
+ )
diff --git a/assets/docs/components/marker.md b/assets/docs/components/marker.md
new file mode 100644
index 0000000..0d6ee7d
--- /dev/null
+++ b/assets/docs/components/marker.md
@@ -0,0 +1,413 @@
+
+
+# Marker
+The Marker component displays inline conversation markers such as status updates, system notes, bordered rows, and labeled separators.
+
+
+# Installation
+Copy the following code into your app directory.
+
+### CLI
+
+```bash
+buridan add component marker
+```
+
+### Manual Installation
+
+```python
+"""Marker component — a flexible inline label with icon and content slots."""
+
+from typing import Literal
+
+import reflex as rx
+from reflex.components.component import ComponentNamespace
+
+from ..utils.twmerge import cn
+
+MarkerVariant = Literal["default", "separator", "border"]
+
+
+class ClassNames:
+ ROOT = (
+ "group/marker relative flex min-h-4 w-full items-center gap-2 "
+ "text-left text-sm text-muted-foreground"
+ )
+
+ VARIANTS: dict[str, str] = {
+ "default": "",
+ "separator": (
+ "before:mr-1 before:h-px before:min-w-0 before:flex-1 before:bg-border "
+ "after:ml-1 after:h-px after:min-w-0 after:flex-1 after:bg-border"
+ ),
+ "border": "border-b border-border pb-2",
+ }
+
+ ICON = "size-4 shrink-0"
+ CONTENT = (
+ "min-w-0 break-words "
+ "group-data-[variant=separator]/marker:flex-none "
+ "group-data-[variant=separator]/marker:text-center"
+ )
+
+
+def marker_root(
+ *children,
+ variant: MarkerVariant = "default",
+ class_name: str = "",
+ **props,
+) -> rx.Component:
+ """Root marker container."""
+ return rx.el.div(
+ *children,
+ data_slot="marker",
+ data_variant=variant,
+ class_name=cn(
+ ClassNames.ROOT,
+ ClassNames.VARIANTS.get(variant, ""),
+ class_name,
+ ),
+ **props,
+ )
+
+
+def marker_icon(*children, class_name: str = "", **props) -> rx.Component:
+ """Icon slot — wraps any icon at a fixed size-4."""
+ return rx.el.span(
+ *children,
+ data_slot="marker-icon",
+ aria_hidden="true",
+ class_name=cn(ClassNames.ICON, class_name),
+ **props,
+ )
+
+
+def marker_content(*children, class_name: str = "", **props) -> rx.Component:
+ """Content slot — handles text wrapping and separator alignment."""
+ return rx.el.span(
+ *children,
+ data_slot="marker-content",
+ class_name=cn(ClassNames.CONTENT, class_name),
+ **props,
+ )
+
+
+class Marker(ComponentNamespace):
+ """Marker namespace."""
+
+ root = staticmethod(marker_root)
+ icon = staticmethod(marker_icon)
+ content = staticmethod(marker_content)
+
+ class_names = ClassNames
+
+
+marker = Marker()
+```
+
+
+# Anatomy
+Use the following composition to build a `Marker` component.
+
+
+```python
+marker.root(
+ marker.icon(),
+ marker.content(),
+)
+```
+
+
+# Examples
+
+## Variants
+
+Use `variant` to switch between an inline marker, bordered row, and labeled separator.
+
+
+```python
+def marker_variants_demo():
+ return rx.el.div(
+ # Default Marker
+ marker.root(
+ marker.content("A default marker for inline notes."),
+ ),
+ # Separator Marker
+ marker.root(
+ marker.content("A separator marker"),
+ variant="separator",
+ ),
+ # Border Marker
+ marker.root(
+ marker.content("A border marker for row boundaries."),
+ variant="border",
+ ),
+ class_name="flex w-full max-w-sm flex-col gap-8 py-12",
+ )
+```
+
+
+| Variant | Description |
+| ----------- | ---------------------------------------------------- |
+| `default` | An inline marker for status, notes, and actions. |
+| `border` | A default marker with a bottom border under the row. |
+| `separator` | A centered label with divider lines on each side. |
+
+## Status
+
+Set `role="status"` and include a [`Spinner`](/docs/components/spinner) for streaming or in-progress markers so updates are announced.
+
+
+```python
+def marker_status_demo():
+ return rx.el.div(
+ marker.root(
+ marker.icon(spinner()),
+ marker.content("Compacting conversation"),
+ role="status",
+ ),
+ marker.root(
+ marker.icon(spinner()),
+ marker.content("Running tests"),
+ variant="separator",
+ role="status",
+ ),
+ class_name="flex w-full max-w-sm flex-col gap-8 py-12",
+ )
+```
+
+
+## Shimmer
+
+Add the [`shimmer`](/docs/utilities/shimmer) utility class to `marker.content` for an animated streaming-text effect. The utility ships with the `buridan` package — see the shimmer docs for installation.
+
+
+```python
+def marker_shimmer():
+ return rx.el.div(
+ marker.root(
+ marker.content("Thinking...", class_name="shimmer"),
+ role="status",
+ ),
+ marker.root(
+ marker.content("Reading 4 files", class_name="shimmer"),
+ variant="separator",
+ role="status",
+ ),
+ class_name="flex w-full max-w-sm flex-col gap-8 py-12",
+ )
+```
+
+
+## Separator
+
+Use the `separator` variant for labeled dividers, such as dates or section breaks, in a conversation.
+
+
+```python
+def marker_separator():
+ return rx.el.div(
+ marker.root(
+ marker.content("Today"),
+ variant="separator",
+ ),
+ marker.root(
+ marker.content("Worked for 42s"),
+ variant="separator",
+ ),
+ marker.root(
+ marker.content("Conversation compacted"),
+ variant="separator",
+ ),
+ class_name="flex w-full max-w-sm flex-col gap-8 py-12",
+ )
+```
+
+
+## Border
+
+Use the `border` variant for status rows that should keep the default marker alignment while separating the next row.
+
+
+```python
+def marker_border():
+ return rx.el.div(
+ marker.root(
+ marker.icon(hi("GitBranchIcon")),
+ marker.content("Switched to release-candidate"),
+ variant="border",
+ ),
+ marker.root(
+ marker.icon(hi("Search01Icon")),
+ marker.content("Reviewed 8 related files"),
+ variant="border",
+ ),
+ marker.root(
+ marker.icon(hi("File01Icon")),
+ marker.content("Opened implementation notes"),
+ variant="border",
+ ),
+ class_name="flex w-full max-w-sm flex-col gap-3 py-12",
+ )
+```
+
+
+## With Icon
+
+Use `marker.icon` to render an icon alongside the content. Use `flex-col` to stack the icon above the content.
+
+
+```python
+def marker_with_icon():
+ return rx.el.div(
+ marker.root(
+ marker.icon(hi("GitBranchIcon")),
+ marker.content("Switched to a new branch"),
+ ),
+ marker.root(
+ marker.icon(hi("Search01Icon")),
+ marker.content("Explored 4 files"),
+ variant="separator",
+ ),
+ marker.root(
+ marker.icon(hi("BookOpenCheckIcon")),
+ marker.content("Syncing completed"),
+ class_name="flex-col",
+ ),
+ class_name="flex w-full max-w-sm flex-col gap-12 py-12",
+ )
+```
+
+
+## Links and Buttons
+
+Turn a marker into a link or button with.
+
+
+```python
+def marker_link_button():
+ return rx.el.div(
+ marker.root(
+ rx.el.a(
+ marker.icon(hi("GitBranchIcon")),
+ marker.content("View the pull request"),
+ href="#links-and-buttons",
+ class_name="group flex flex-row items-center gap-x-2 underline transition-colors hover:text-foreground",
+ ),
+ variant="default",
+ ),
+ marker.root(
+ rx.el.button(
+ marker.icon(
+ hi("ArrowMoveUpRightIcon"),
+ class_name="group-hover:text-foreground transition-colors",
+ ),
+ marker.content(
+ "Revert this change",
+ class_name="group-hover:text-foreground transition-colors",
+ ),
+ type="button",
+ class_name="group flex flex-row items-center gap-x-2 transition-colors",
+ on_click=rx.toast("You clicked the revert button"),
+ ),
+ variant="default",
+ ),
+ class_name="flex w-full max-w-sm flex-col gap-8 py-12 justify-center",
+ )
+```
+
+
+## Accessibility
+
+`marker.root` is presentational by default. The correct semantics depend on how you use it, so choose the role based on intent rather than relying on a single default.
+
+## Status and Progress
+
+For streaming or progress markers such as "Thinking..." or a running tool, set `role="status"` so assistive tech announces the update as it appears. `marker.root` forwards `role` to the underlying element.
+
+```python
+marker.root(
+ marker.icon(spinner()),
+ marker.content("Compacting conversation"),
+ role="status",
+)
+```
+
+## Labeled Separators
+
+A `separator` that carries text, such as a date or a section label, needs no role. The divider lines are decorative CSS pseudo-elements, and the text is announced as ordinary content.
+
+```python
+marker.root(
+ marker.content("Today"),
+ variant="separator",
+)
+```
+
+> **Note:** Do not add `role="separator"` to a labeled divider. A separator takes its accessible name from `aria-label`, not from its text, and its contents are treated as presentational, so the visible label would not be announced. Reserve `role="separator"` for a divider with no meaningful text.
+
+## Bordered Markers
+
+A bordered marker keeps the same semantics as the default marker. The bottom border is decorative, so choose `role="status"` or no role based on the marker's purpose.
+
+```python
+marker.root(
+ marker.icon(rx.icon("file-text", size=14)),
+ marker.content("Opened implementation notes"),
+ variant="border",
+)
+```
+
+## Decorative Icons
+
+`marker.icon` is decorative and hidden from assistive tech with `aria-hidden`, so the adjacent `marker.content` carries the meaning. For an icon-only marker, provide an `aria_label` so it is not announced as empty.
+
+```python
+marker.root(
+ marker.icon(rx.icon("check", size=14)),
+ aria_label="Synced",
+)
+```
+
+## Interactive Markers
+
+When a marker links or triggers an action, render it as an `rx.link` or pass `on_click` so it is focusable and exposes the correct role.
+
+```python
+rx.link(
+ marker.root(
+ marker.icon(rx.icon("file-text", size=14)),
+ marker.content("Explored 4 files"),
+ ),
+ href="/files",
+)
+```
+
+# API Reference
+
+## marker.root
+
+The root marker element.
+
+| Prop | Type | Default | Description |
+|--------------|-------------------------------------------|-------------|--------------------------------------------------|
+| `variant` | `"default" \| "border" \| "separator"` | `"default"` | The marker layout. |
+| `class_name` | `str` | `""` | Additional classes to apply to the root element. |
+| `**props` | `dict` | — | Any valid HTML attribute (`role`, `aria_label`). |
+
+## marker.icon
+
+A decorative icon slot. Hidden from assistive tech with `aria-hidden`.
+
+| Prop | Type | Default | Description |
+|--------------|-------|---------|-----------------------------------------------|
+| `class_name` | `str` | `""` | Additional classes to apply to the icon slot. |
+
+## marker.content
+
+The marker text content.
+
+| Prop | Type | Default | Description |
+|--------------|-------|---------|--------------------------------------------------|
+| `class_name` | `str` | `""` | Additional classes to apply to the content slot. |
+| `**props` | `dict`| — | Any valid HTML attribute forwarded to the span. |
diff --git a/assets/docs/components/spinner.md b/assets/docs/components/spinner.md
new file mode 100644
index 0000000..d859d03
--- /dev/null
+++ b/assets/docs/components/spinner.md
@@ -0,0 +1,146 @@
+
+
+# Spinner
+An indicator that can be used to show a loading state.
+
+> **Note:** The Spinner component is a fully custom implementation using in-line **svg** with no external dependencies.
+
+# Installation
+Copy the following code into your app directory.
+
+### CLI
+
+```bash
+buridan add component spinner
+```
+
+### Manual Installation
+
+```python
+"""Spinner component."""
+
+import reflex as rx
+from reflex_components_core.el import svg
+
+from ..utils.twmerge import cn
+
+
+def spinner(class_name: str = "", **props) -> rx.Component:
+ return svg(
+ svg.path(
+ opacity="0.2",
+ d="M14.66 8a6.666 6.666 0 1 1-13.333 0 6.666 6.666 0 0 1 13.333 0Z",
+ stroke="currentColor",
+ stroke_width="1.5",
+ ),
+ svg.path(
+ d="M13.413 11.877A6.666 6.666 0 1 1 10.26 1.728",
+ stroke="currentColor",
+ stroke_width="1.5",
+ ),
+ xmlns="http://www.w3.org/2000/svg",
+ custom_attrs={"viewBox": "0 0 16 16"},
+ class_name=cn("size-4 animate-spin fill-none", class_name),
+ data_slot="spinner",
+ role="status",
+ **props,
+ )
+```
+
+
+# Anatomy
+Use the following composition to build a `Spinner` component.
+
+
+```python
+spinner()
+```
+
+
+# Examples
+
+## Size
+
+Use the `size-*` utility class to change the size of the spinner.
+
+
+```python
+def spinner_size():
+ return rx.el.div(
+ spinner(class_name="size-3"),
+ spinner(class_name="size-4"),
+ spinner(class_name="size-6"),
+ spinner(class_name="size-8"),
+ class_name="flex items-center gap-6",
+ )
+```
+
+
+## Button
+
+Add a spinner to a button to indicate a loading state. Place it before the label for a start position.
+
+
+```python
+def spinner_button():
+ return rx.el.div(
+ button(spinner(), "Loading...", disabled=True, size="sm"),
+ button(spinner(), "Please wait", disabled=True, size="sm", variant="outline"),
+ button(spinner(), "Processing", disabled=True, size="sm", variant="secondary"),
+ class_name="flex flex-col items-center gap-4",
+ )
+```
+
+
+## Badge
+
+Add a spinner to a badge to indicate a loading or syncing state.
+
+
+```python
+def spinner_badge():
+ return rx.el.div(
+ badge(spinner(), "Syncing"),
+ badge(spinner(), "Updating", variant="secondary"),
+ badge(spinner(), "Processing", variant="outline"),
+ class_name="flex items-center gap-4",
+ )
+```
+
+
+## Marker
+
+Combine `Spinner` with `Marker` and the `shimmer` utility for animated streaming status indicators. Set `role="status"` so assistive technology announces the update.
+
+
+```python
+def spinner_marker():
+ return rx.el.div(
+ marker.root(
+ marker.icon(spinner()),
+ marker.content("Thinking…", class_name="shimmer w-fit"),
+ role="status",
+ ),
+ marker.root(
+ marker.icon(spinner()),
+ marker.content("Generating response…", class_name="shimmer w-fit"),
+ variant="border",
+ role="status",
+ ),
+ marker.root(
+ marker.icon(spinner()),
+ marker.content("Processing"),
+ variant="separator",
+ role="status",
+ ),
+ class_name="flex w-full max-w-sm flex-col gap-6",
+ )
+```
+
+
+# API Reference
+
+| Prop | Type | Default | Description |
+|--------------|--------|---------|--------------------------------------------------|
+| `class_name` | `str` | `""` | Additional Tailwind classes applied to the icon. |
+| `**props` | `dict` | — | Any valid HTML attribute forwarded to the element (`role`, `aria_label`, etc.). |
diff --git a/assets/docs/getting-started/cli.md b/assets/docs/getting-started/cli.md
index 9a9544b..123822c 100644
--- a/assets/docs/getting-started/cli.md
+++ b/assets/docs/getting-started/cli.md
@@ -4,7 +4,7 @@
Use the buridan CLI to add components, apply themes, and manage your Buridan UI project.
-## Installation
+# Installation
```bash
pip install buridan-create
@@ -12,7 +12,7 @@ pip install buridan-create
All commands must be run from your Reflex project root, where `rxconfig.py` is located.
-## create
+# create
Open the Buridan UI theme builder in your browser. Use it to customize your design system and generate a unique preset ID.
@@ -20,7 +20,7 @@ Open the Buridan UI theme builder in your browser. Use it to customize your desi
buridan create
```
-## init
+# init
Initialize Buridan UI in your project. This command sets up CSS utilities (shimmer, scrollbar) in `assets/globals.css` and updates `rxconfig.py` with the required Tailwind configuration.
@@ -28,7 +28,7 @@ Initialize Buridan UI in your project. This command sets up CSS utilities (shimm
buridan init
```
-## apply
+# apply
Apply a theme preset to your project. Generates `:root` and `.dark` CSS variable blocks in `assets/globals.css` based on the preset ID from the theme builder.
@@ -46,7 +46,7 @@ buridan apply --preset b0
buridan apply --preset b2D0wqNxT
```
-## add
+# add
Add components and their dependencies to your project.
@@ -70,7 +70,7 @@ Components are placed in `components/`, blocks in `blocks/`. Dependencies are re
> **Note:** Components require a theme to render correctly. Run `buridan apply` before using components.
-## list
+# list
Display all available components and blocks.
@@ -78,7 +78,7 @@ Display all available components and blocks.
buridan list
```
-## Recommended workflow
+# Recommended workflow
```bash
buridan create # build your theme, copy the preset ID
diff --git a/assets/fuse/searchList.json b/assets/fuse/searchList.json
index 9fb1de1..0fb90bc 100644
--- a/assets/fuse/searchList.json
+++ b/assets/fuse/searchList.json
@@ -154,6 +154,11 @@
"title": "Kbd",
"url": "docs/components/kbd"
},
+ {
+ "section": "Components",
+ "title": "Marker",
+ "url": "docs/components/marker"
+ },
{
"section": "Components",
"title": "Menu",
@@ -184,6 +189,11 @@
"title": "Slider",
"url": "docs/components/slider"
},
+ {
+ "section": "Components",
+ "title": "Spinner",
+ "url": "docs/components/spinner"
+ },
{
"section": "Components",
"title": "Tabs",
diff --git a/assets/llms.txt b/assets/llms.txt
index 588f21d..ead87fd 100644
--- a/assets/llms.txt
+++ b/assets/llms.txt
@@ -35,12 +35,14 @@
- [Input](https://buridan.reflex.run/docs/components/input): The Input component.
- [Input Group](https://buridan.reflex.run/docs/components/input-group): The Input Group component.
- [Kbd](https://buridan.reflex.run/docs/components/kbd): The Kbd component.
+- [Marker](https://buridan.reflex.run/docs/components/marker): The Marker component.
- [Menu](https://buridan.reflex.run/docs/components/menu): The Menu component.
- [Popover](https://buridan.reflex.run/docs/components/popover): The Popover component.
- [Scroll Area](https://buridan.reflex.run/docs/components/scroll-area): The Scroll Area component.
- [Select](https://buridan.reflex.run/docs/components/select): The Select component.
- [Skeleton](https://buridan.reflex.run/docs/components/skeleton): The Skeleton component.
- [Slider](https://buridan.reflex.run/docs/components/slider): The Slider component.
+- [Spinner](https://buridan.reflex.run/docs/components/spinner): The Spinner component.
- [Tabs](https://buridan.reflex.run/docs/components/tabs): The Tabs component.
- [Textarea](https://buridan.reflex.run/docs/components/textarea): The Textarea component.
- [Timeline](https://buridan.reflex.run/docs/components/timeline): The Timeline component.
diff --git a/assets/sitemap.xml b/assets/sitemap.xml
index ef16218..a087202 100644
--- a/assets/sitemap.xml
+++ b/assets/sitemap.xml
@@ -32,12 +32,14 @@
https://buridan.reflex.run/docs/components/input
https://buridan.reflex.run/docs/components/input-group
https://buridan.reflex.run/docs/components/kbd
+ https://buridan.reflex.run/docs/components/marker
https://buridan.reflex.run/docs/components/menu
https://buridan.reflex.run/docs/components/popover
https://buridan.reflex.run/docs/components/scroll-area
https://buridan.reflex.run/docs/components/select
https://buridan.reflex.run/docs/components/skeleton
https://buridan.reflex.run/docs/components/slider
+ https://buridan.reflex.run/docs/components/spinner
https://buridan.reflex.run/docs/components/tabs
https://buridan.reflex.run/docs/components/textarea
https://buridan.reflex.run/docs/components/timeline
diff --git a/assets/social/marker.webp b/assets/social/marker.webp
new file mode 100644
index 0000000..2641a89
Binary files /dev/null and b/assets/social/marker.webp differ
diff --git a/assets/social/spinner.webp b/assets/social/spinner.webp
new file mode 100644
index 0000000..4fc8447
Binary files /dev/null and b/assets/social/spinner.webp differ
diff --git a/components/ui/marker.py b/components/ui/marker.py
new file mode 100644
index 0000000..eae943e
--- /dev/null
+++ b/components/ui/marker.py
@@ -0,0 +1,87 @@
+"""Marker component — a flexible inline label with icon and content slots."""
+
+from typing import Literal
+
+import reflex as rx
+from reflex.components.component import ComponentNamespace
+
+from ..utils.twmerge import cn
+
+MarkerVariant = Literal["default", "separator", "border"]
+
+
+class ClassNames:
+ ROOT = (
+ "group/marker relative flex min-h-4 w-full items-center gap-2 "
+ "text-left text-sm text-muted-foreground"
+ )
+
+ VARIANTS: dict[str, str] = {
+ "default": "",
+ "separator": (
+ "before:mr-1 before:h-px before:min-w-0 before:flex-1 before:bg-border "
+ "after:ml-1 after:h-px after:min-w-0 after:flex-1 after:bg-border"
+ ),
+ "border": "border-b border-border pb-2",
+ }
+
+ ICON = "size-4 shrink-0"
+ CONTENT = (
+ "min-w-0 break-words "
+ "group-data-[variant=separator]/marker:flex-none "
+ "group-data-[variant=separator]/marker:text-center"
+ )
+
+
+def marker_root(
+ *children,
+ variant: MarkerVariant = "default",
+ class_name: str = "",
+ **props,
+) -> rx.Component:
+ """Root marker container."""
+ return rx.el.div(
+ *children,
+ data_slot="marker",
+ data_variant=variant,
+ class_name=cn(
+ ClassNames.ROOT,
+ ClassNames.VARIANTS.get(variant, ""),
+ class_name,
+ ),
+ **props,
+ )
+
+
+def marker_icon(*children, class_name: str = "", **props) -> rx.Component:
+ """Icon slot — wraps any icon at a fixed size-4."""
+ return rx.el.span(
+ *children,
+ data_slot="marker-icon",
+ aria_hidden="true",
+ class_name=cn(ClassNames.ICON, class_name),
+ **props,
+ )
+
+
+def marker_content(*children, class_name: str = "", **props) -> rx.Component:
+ """Content slot — handles text wrapping and separator alignment."""
+ return rx.el.span(
+ *children,
+ data_slot="marker-content",
+ class_name=cn(ClassNames.CONTENT, class_name),
+ **props,
+ )
+
+
+class Marker(ComponentNamespace):
+ """Marker namespace."""
+
+ root = staticmethod(marker_root)
+ icon = staticmethod(marker_icon)
+ content = staticmethod(marker_content)
+
+ class_names = ClassNames
+
+
+marker = Marker()
diff --git a/components/ui/spinner.py b/components/ui/spinner.py
new file mode 100644
index 0000000..bc320de
--- /dev/null
+++ b/components/ui/spinner.py
@@ -0,0 +1,28 @@
+"""Spinner component."""
+
+import reflex as rx
+from reflex_components_core.el import svg
+
+from ..utils.twmerge import cn
+
+
+def spinner(class_name: str = "", **props) -> rx.Component:
+ return svg(
+ svg.path(
+ opacity="0.2",
+ d="M14.66 8a6.666 6.666 0 1 1-13.333 0 6.666 6.666 0 0 1 13.333 0Z",
+ stroke="currentColor",
+ stroke_width="1.5",
+ ),
+ svg.path(
+ d="M13.413 11.877A6.666 6.666 0 1 1 10.26 1.728",
+ stroke="currentColor",
+ stroke_width="1.5",
+ ),
+ xmlns="http://www.w3.org/2000/svg",
+ custom_attrs={"viewBox": "0 0 16 16"},
+ class_name=cn("size-4 animate-spin fill-none", class_name),
+ data_slot="spinner",
+ role="status",
+ **props,
+ )
diff --git a/docs/components/marker.md b/docs/components/marker.md
new file mode 100644
index 0000000..9b50613
--- /dev/null
+++ b/docs/components/marker.md
@@ -0,0 +1,164 @@
+---
+title: "Marker"
+description: "Displays an inline status, system note, bordered row, or labeled separator in a conversation."
+order: 0
+---
+
+# Marker
+The Marker component displays inline conversation markers such as status updates, system notes, bordered rows, and labeled separators.
+
+
+# Installation
+Copy the following code into your app directory.
+
+--INSTALL(marker)--
+
+# Anatomy
+Use the following composition to build a `Marker` component.
+
+--ANATOMY(marker)--
+
+# Examples
+
+## Variants
+
+Use `variant` to switch between an inline marker, bordered row, and labeled separator.
+
+--DEMO(marker_variants_demo)--
+
+| Variant | Description |
+| ----------- | ---------------------------------------------------- |
+| `default` | An inline marker for status, notes, and actions. |
+| `border` | A default marker with a bottom border under the row. |
+| `separator` | A centered label with divider lines on each side. |
+
+## Status
+
+Set `role="status"` and include a [`Spinner`](/docs/components/spinner) for streaming or in-progress markers so updates are announced.
+
+--DEMO(marker_status_demo)--
+
+## Shimmer
+
+Add the [`shimmer`](/docs/utilities/shimmer) utility class to `marker.content` for an animated streaming-text effect. The utility ships with the `buridan` package — see the shimmer docs for installation.
+
+--DEMO(marker_shimmer)--
+
+## Separator
+
+Use the `separator` variant for labeled dividers, such as dates or section breaks, in a conversation.
+
+--DEMO(marker_separator)--
+
+## Border
+
+Use the `border` variant for status rows that should keep the default marker alignment while separating the next row.
+
+--DEMO(marker_border)--
+
+## With Icon
+
+Use `marker.icon` to render an icon alongside the content. Use `flex-col` to stack the icon above the content.
+
+--DEMO(marker_with_icon)--
+
+## Links and Buttons
+
+Turn a marker into a link or button with.
+
+--DEMO(marker_link_button)--
+
+## Accessibility
+
+`marker.root` is presentational by default. The correct semantics depend on how you use it, so choose the role based on intent rather than relying on a single default.
+
+## Status and Progress
+
+For streaming or progress markers such as "Thinking..." or a running tool, set `role="status"` so assistive tech announces the update as it appears. `marker.root` forwards `role` to the underlying element.
+
+```python
+marker.root(
+ marker.icon(spinner()),
+ marker.content("Compacting conversation"),
+ role="status",
+)
+```
+
+## Labeled Separators
+
+A `separator` that carries text, such as a date or a section label, needs no role. The divider lines are decorative CSS pseudo-elements, and the text is announced as ordinary content.
+
+```python
+marker.root(
+ marker.content("Today"),
+ variant="separator",
+)
+```
+
+> **Note:** Do not add `role="separator"` to a labeled divider. A separator takes its accessible name from `aria-label`, not from its text, and its contents are treated as presentational, so the visible label would not be announced. Reserve `role="separator"` for a divider with no meaningful text.
+
+## Bordered Markers
+
+A bordered marker keeps the same semantics as the default marker. The bottom border is decorative, so choose `role="status"` or no role based on the marker's purpose.
+
+```python
+marker.root(
+ marker.icon(rx.icon("file-text", size=14)),
+ marker.content("Opened implementation notes"),
+ variant="border",
+)
+```
+
+## Decorative Icons
+
+`marker.icon` is decorative and hidden from assistive tech with `aria-hidden`, so the adjacent `marker.content` carries the meaning. For an icon-only marker, provide an `aria_label` so it is not announced as empty.
+
+```python
+marker.root(
+ marker.icon(rx.icon("check", size=14)),
+ aria_label="Synced",
+)
+```
+
+## Interactive Markers
+
+When a marker links or triggers an action, render it as an `rx.link` or pass `on_click` so it is focusable and exposes the correct role.
+
+```python
+rx.link(
+ marker.root(
+ marker.icon(rx.icon("file-text", size=14)),
+ marker.content("Explored 4 files"),
+ ),
+ href="/files",
+)
+```
+
+# API Reference
+
+## marker.root
+
+The root marker element.
+
+| Prop | Type | Default | Description |
+|--------------|-------------------------------------------|-------------|--------------------------------------------------|
+| `variant` | `"default" \| "border" \| "separator"` | `"default"` | The marker layout. |
+| `class_name` | `str` | `""` | Additional classes to apply to the root element. |
+| `**props` | `dict` | — | Any valid HTML attribute (`role`, `aria_label`). |
+
+## marker.icon
+
+A decorative icon slot. Hidden from assistive tech with `aria-hidden`.
+
+| Prop | Type | Default | Description |
+|--------------|-------|---------|-----------------------------------------------|
+| `class_name` | `str` | `""` | Additional classes to apply to the icon slot. |
+
+## marker.content
+
+The marker text content.
+
+| Prop | Type | Default | Description |
+|--------------|-------|---------|--------------------------------------------------|
+| `class_name` | `str` | `""` | Additional classes to apply to the content slot. |
+| `**props` | `dict`| — | Any valid HTML attribute forwarded to the span. |
diff --git a/docs/components/spinner.md b/docs/components/spinner.md
new file mode 100644
index 0000000..63185a6
--- /dev/null
+++ b/docs/components/spinner.md
@@ -0,0 +1,53 @@
+---
+title: "Spinner"
+description: "An indicator that can be used to show a loading state."
+order: 0
+---
+
+# Spinner
+An indicator that can be used to show a loading state.
+
+> **Note:** The Spinner component is a fully custom implementation using in-line **svg** with no external dependencies.
+
+# Installation
+Copy the following code into your app directory.
+
+--INSTALL(spinner)--
+
+# Anatomy
+Use the following composition to build a `Spinner` component.
+
+--ANATOMY(spinner)--
+
+# Examples
+
+## Size
+
+Use the `size-*` utility class to change the size of the spinner.
+
+--DEMO(spinner_size)--
+
+## Button
+
+Add a spinner to a button to indicate a loading state. Place it before the label for a start position.
+
+--DEMO(spinner_button)--
+
+## Badge
+
+Add a spinner to a badge to indicate a loading or syncing state.
+
+--DEMO(spinner_badge)--
+
+## Marker
+
+Combine `Spinner` with `Marker` and the `shimmer` utility for animated streaming status indicators. Set `role="status"` so assistive technology announces the update.
+
+--DEMO(spinner_marker)--
+
+# API Reference
+
+| Prop | Type | Default | Description |
+|--------------|--------|---------|--------------------------------------------------|
+| `class_name` | `str` | `""` | Additional Tailwind classes applied to the icon. |
+| `**props` | `dict` | — | Any valid HTML attribute forwarded to the element (`role`, `aria_label`, etc.). |
diff --git a/docs/getting_started/cli.md b/docs/getting_started/cli.md
index c3d3eb1..ed8b7ac 100644
--- a/docs/getting_started/cli.md
+++ b/docs/getting_started/cli.md
@@ -8,7 +8,7 @@ order: 3
Use the buridan CLI to add components, apply themes, and manage your Buridan UI project.
-## Installation
+# Installation
```bash
pip install buridan-create
@@ -16,7 +16,7 @@ pip install buridan-create
All commands must be run from your Reflex project root, where `rxconfig.py` is located.
-## create
+# create
Open the Buridan UI theme builder in your browser. Use it to customize your design system and generate a unique preset ID.
@@ -24,7 +24,7 @@ Open the Buridan UI theme builder in your browser. Use it to customize your desi
buridan create
```
-## init
+# init
Initialize Buridan UI in your project. This command sets up CSS utilities (shimmer, scrollbar) in `assets/globals.css` and updates `rxconfig.py` with the required Tailwind configuration.
@@ -32,7 +32,7 @@ Initialize Buridan UI in your project. This command sets up CSS utilities (shimm
buridan init
```
-## apply
+# apply
Apply a theme preset to your project. Generates `:root` and `.dark` CSS variable blocks in `assets/globals.css` based on the preset ID from the theme builder.
@@ -50,7 +50,7 @@ buridan apply --preset b0
buridan apply --preset b2D0wqNxT
```
-## add
+# add
Add components and their dependencies to your project.
@@ -74,7 +74,7 @@ Components are placed in `components/`, blocks in `blocks/`. Dependencies are re
> **Note:** Components require a theme to render correctly. Run `buridan apply` before using components.
-## list
+# list
Display all available components and blocks.
@@ -82,7 +82,7 @@ Display all available components and blocks.
buridan list
```
-## Recommended workflow
+# Recommended workflow
```bash
buridan create # build your theme, copy the preset ID