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