diff --git a/app/templates/docsidebar.py b/app/templates/docsidebar.py
index f3678ea..60ba8c3 100644
--- a/app/templates/docsidebar.py
+++ b/app/templates/docsidebar.py
@@ -20,6 +20,7 @@ class SidebarSection:
SIDEBAR_SECTIONS = [
SidebarSection(title="Getting Started", routes=routes.GET_STARTED_URLS),
SidebarSection(title="Resources", routes=routes.RESOURCES_URLS),
+ SidebarSection(title="Utilities", routes=routes.UTILITIES),
SidebarSection(title="Charts", routes=routes.CHARTS_URLS),
SidebarSection(title="Components", routes=routes.BASE_UI_COMPONENTS),
]
diff --git a/app/utils/routes.py b/app/utils/routes.py
index 2310413..0810210 100644
--- a/app/utils/routes.py
+++ b/app/utils/routes.py
@@ -65,6 +65,7 @@ def generate_doc_routes(section_folder, base_path) -> list[dict]:
"",
[{"title": "llms.txt", "url": "llms.txt", "order": "5"}],
),
+ ("utilities", "docs/utilities/", "title", []),
("resources", "docs/resources/", "title", []),
("components", "docs/components/", "title", []),
("charts", "docs/charts/", "title", []),
@@ -90,7 +91,7 @@ def build_all_routes():
RESOURCES_URLS = ALL_ROUTES["resources"]
BASE_UI_COMPONENTS = ALL_ROUTES["components"]
CHARTS_URLS = ALL_ROUTES["charts"]
-
+UTILITIES = ALL_ROUTES["utilities"]
if __name__ == "__main__":
print(ALL_ROUTES)
diff --git a/app/www/library/utilities/scroll_fade_demo.py b/app/www/library/utilities/scroll_fade_demo.py
new file mode 100644
index 0000000..2316ce8
--- /dev/null
+++ b/app/www/library/utilities/scroll_fade_demo.py
@@ -0,0 +1,20 @@
+import reflex as rx
+
+
+def scroll_fade_demo():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ *[
+ rx.el.div(
+ f"Item {index + 1}",
+ class_name="rounded-lg bg-muted px-3 py-2.5 text-sm",
+ )
+ for index in range(12)
+ ],
+ class_name="flex flex-col gap-1.5 p-1.5",
+ ),
+ class_name="h-72 scroll-fade scrollbar-none overflow-y-auto",
+ ),
+ class_name="mx-auto w-full max-w-xs overflow-hidden rounded-2xl border border-input",
+ )
diff --git a/app/www/library/utilities/scroll_fade_disabled.py b/app/www/library/utilities/scroll_fade_disabled.py
new file mode 100644
index 0000000..48ba781
--- /dev/null
+++ b/app/www/library/utilities/scroll_fade_disabled.py
@@ -0,0 +1,48 @@
+import reflex as rx
+
+
+def scroll_fade_none_items():
+ return rx.el.div(
+ *[
+ rx.el.div(
+ f"Item {index + 1}",
+ class_name="rounded-lg bg-muted px-3 py-2.5 text-sm",
+ )
+ for index in range(8)
+ ],
+ class_name="flex flex-col gap-1.5 p-1.5",
+ )
+
+
+def scroll_fade_none():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ scroll_fade_none_items(),
+ class_name="h-48 scroll-fade scrollbar-none overflow-y-auto",
+ ),
+ class_name="overflow-hidden rounded-2xl border border-input",
+ ),
+ rx.el.p(
+ "scroll-fade",
+ class_name="text-center font-mono text-xs text-muted-foreground",
+ ),
+ class_name="flex flex-col gap-3",
+ ),
+ rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ scroll_fade_none_items(),
+ class_name="h-48 scroll-fade scrollbar-none overflow-y-auto scroll-fade-none",
+ ),
+ class_name="overflow-hidden rounded-2xl border border-input",
+ ),
+ rx.el.p(
+ "scroll-fade scroll-fade-none",
+ class_name="text-center font-mono text-xs text-muted-foreground",
+ ),
+ class_name="flex flex-col gap-3",
+ ),
+ class_name="mx-auto flex w-full max-w-xs min-w-0 flex-col gap-6",
+ )
diff --git a/app/www/library/utilities/scroll_fade_edges.py b/app/www/library/utilities/scroll_fade_edges.py
new file mode 100644
index 0000000..bc18f5c
--- /dev/null
+++ b/app/www/library/utilities/scroll_fade_edges.py
@@ -0,0 +1,109 @@
+import reflex as rx
+
+items = [
+ "Inbox triage",
+ "Design review",
+ "API contract",
+ "QA pass",
+ "Launch notes",
+ "Metrics follow-up",
+]
+
+tags = [
+ "Design",
+ "Engineering",
+ "Marketing",
+ "Product",
+ "Research",
+ "Sales",
+ "Support",
+ "Operations",
+]
+
+
+def scroll_fade_edge_items():
+ return rx.el.div(
+ *[
+ rx.el.div(
+ item,
+ class_name="rounded-lg bg-muted px-3 py-2.5 text-sm",
+ )
+ for item in items
+ ],
+ class_name="flex flex-col gap-1.5 p-1.5",
+ )
+
+
+def scroll_fade_edge_tags():
+ return rx.el.div(
+ *[
+ rx.el.div(
+ tag,
+ class_name="shrink-0 rounded-xl bg-muted px-4 py-2.5 text-sm",
+ )
+ for tag in tags
+ ],
+ class_name="flex w-max gap-1.5 p-1.5",
+ )
+
+
+def scroll_fade_edge():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ scroll_fade_edge_items(),
+ class_name="h-36 scroll-fade-t scrollbar-none overflow-y-auto",
+ ),
+ class_name="overflow-hidden rounded-2xl border border-input",
+ ),
+ rx.el.p(
+ "scroll-fade-t",
+ class_name="text-center font-mono text-xs text-muted-foreground",
+ ),
+ class_name="flex flex-col gap-3",
+ ),
+ rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ scroll_fade_edge_items(),
+ class_name="h-36 scroll-fade-b scrollbar-none overflow-y-auto",
+ ),
+ class_name="overflow-hidden rounded-2xl border border-input",
+ ),
+ rx.el.p(
+ "scroll-fade-b",
+ class_name="text-center font-mono text-xs text-muted-foreground",
+ ),
+ class_name="flex flex-col gap-3",
+ ),
+ rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ scroll_fade_edge_tags(),
+ class_name="scroll-fade-s scrollbar-none overflow-x-auto",
+ ),
+ class_name="overflow-hidden rounded-2xl border border-input",
+ ),
+ rx.el.p(
+ "scroll-fade-s",
+ class_name="text-center font-mono text-xs text-muted-foreground",
+ ),
+ class_name="flex flex-col gap-3",
+ ),
+ rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ scroll_fade_edge_tags(),
+ class_name="scroll-fade-e scrollbar-none overflow-x-auto",
+ ),
+ class_name="overflow-hidden rounded-2xl border border-input",
+ ),
+ rx.el.p(
+ "scroll-fade-e",
+ class_name="text-center font-mono text-xs text-muted-foreground",
+ ),
+ class_name="flex flex-col gap-3",
+ ),
+ class_name="mx-auto flex max-w-xs min-w-0 flex-col gap-6",
+ )
diff --git a/app/www/library/utilities/scroll_fade_horizontal.py b/app/www/library/utilities/scroll_fade_horizontal.py
new file mode 100644
index 0000000..1a3a73d
--- /dev/null
+++ b/app/www/library/utilities/scroll_fade_horizontal.py
@@ -0,0 +1,35 @@
+import reflex as rx
+
+tags = [
+ "Design",
+ "Engineering",
+ "Marketing",
+ "Product",
+ "Research",
+ "Sales",
+ "Support",
+ "Operations",
+ "Finance",
+ "Legal",
+ "People",
+ "Security",
+]
+
+
+def scroll_fade_horizontal():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ *[
+ rx.el.div(
+ tag,
+ class_name="shrink-0 rounded-lg bg-muted px-3 py-2.5 text-sm",
+ )
+ for tag in tags
+ ],
+ class_name="flex w-max gap-1.5 p-1.5",
+ ),
+ class_name="scroll-fade-x scrollbar-none overflow-x-auto",
+ ),
+ class_name="mx-auto w-full max-w-xs overflow-hidden rounded-2xl border border-input",
+ )
diff --git a/app/www/library/utilities/scroll_fade_no_fade.py b/app/www/library/utilities/scroll_fade_no_fade.py
new file mode 100644
index 0000000..4741ff1
--- /dev/null
+++ b/app/www/library/utilities/scroll_fade_no_fade.py
@@ -0,0 +1,20 @@
+import reflex as rx
+
+
+def scroll_fade_no_fade():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ *[
+ rx.el.div(
+ f"Item {index + 1}",
+ class_name="rounded-lg bg-muted px-3 py-2.5 text-sm",
+ )
+ for index in range(3)
+ ],
+ class_name="flex flex-col gap-1.5 p-1.5",
+ ),
+ class_name="scroll-fade scrollbar-none overflow-y-auto",
+ ),
+ class_name="mx-auto w-full max-w-xs overflow-hidden rounded-2xl border border-input",
+ )
diff --git a/app/www/library/utilities/scroll_fade_size.py b/app/www/library/utilities/scroll_fade_size.py
new file mode 100644
index 0000000..a366a39
--- /dev/null
+++ b/app/www/library/utilities/scroll_fade_size.py
@@ -0,0 +1,48 @@
+import reflex as rx
+
+
+def scroll_fade_size_items():
+ return rx.el.div(
+ *[
+ rx.el.div(
+ f"Item {index + 1}",
+ class_name="rounded-lg bg-muted px-3 py-2.5 text-sm",
+ )
+ for index in range(8)
+ ],
+ class_name="flex flex-col gap-1.5 p-1.5",
+ )
+
+
+def scroll_fade_size():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ scroll_fade_size_items(),
+ class_name="h-48 scroll-fade scrollbar-none overflow-y-auto scroll-fade-4",
+ ),
+ class_name="overflow-hidden rounded-2xl border border-input",
+ ),
+ rx.el.p(
+ "scroll-fade-4",
+ class_name="text-center font-mono text-xs text-muted-foreground",
+ ),
+ class_name="flex flex-col gap-3",
+ ),
+ rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ scroll_fade_size_items(),
+ class_name="h-48 scroll-fade scrollbar-none overflow-y-auto scroll-fade-24",
+ ),
+ class_name="overflow-hidden rounded-2xl border border-input",
+ ),
+ rx.el.p(
+ "scroll-fade-24",
+ class_name="text-center font-mono text-xs text-muted-foreground",
+ ),
+ class_name="flex flex-col gap-3",
+ ),
+ class_name="mx-auto flex w-full max-w-xs flex-col gap-6",
+ )
diff --git a/app/www/library/utilities/shimmer_angle.py b/app/www/library/utilities/shimmer_angle.py
new file mode 100644
index 0000000..3b3790a
--- /dev/null
+++ b/app/www/library/utilities/shimmer_angle.py
@@ -0,0 +1,17 @@
+import reflex as rx
+
+
+def shimmer_angle():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.p("Generating response...", class_name="shimmer"),
+ rx.el.p("shimmer", class_name="font-mono text-xs"),
+ class_name="flex flex-col gap-3",
+ ),
+ rx.el.div(
+ rx.el.p("Generating response...", class_name="shimmer shimmer-angle-45"),
+ rx.el.p("shimmer-angle-45", class_name="font-mono text-xs"),
+ class_name="flex flex-col gap-3",
+ ),
+ class_name="mx-auto grid w-full max-w-lg gap-6 text-center text-sm text-muted-foreground sm:grid-cols-2",
+ )
diff --git a/app/www/library/utilities/shimmer_color.py b/app/www/library/utilities/shimmer_color.py
new file mode 100644
index 0000000..f128399
--- /dev/null
+++ b/app/www/library/utilities/shimmer_color.py
@@ -0,0 +1,13 @@
+import reflex as rx
+
+
+def shimmer_color():
+ return rx.el.div(
+ rx.el.p(
+ "Generating response", class_name="shimmer shimmer-color-blue-500/60 w-fit"
+ ),
+ rx.el.p(
+ "Generating response", class_name="shimmer shimmer-color-[#378ADD] w-fit"
+ ),
+ class_name="flex flex-col items-center gap-2 text-sm text-muted-foreground",
+ )
diff --git a/app/www/library/utilities/shimmer_duration.py b/app/www/library/utilities/shimmer_duration.py
new file mode 100644
index 0000000..9976a36
--- /dev/null
+++ b/app/www/library/utilities/shimmer_duration.py
@@ -0,0 +1,19 @@
+import reflex as rx
+
+
+def shimmer_duration():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.p("Generating response...", class_name="shimmer"),
+ rx.el.p("shimmer", class_name="font-mono text-xs"),
+ class_name="flex flex-col gap-3",
+ ),
+ rx.el.div(
+ rx.el.p(
+ "Generating response...", class_name="shimmer shimmer-duration-1000"
+ ),
+ rx.el.p("shimmer-duration-1000", class_name="font-mono text-xs"),
+ class_name="flex flex-col gap-3",
+ ),
+ class_name="mx-auto grid w-full max-w-lg gap-6 text-center text-sm text-muted-foreground sm:grid-cols-2",
+ )
diff --git a/app/www/library/utilities/shimmer_none.py b/app/www/library/utilities/shimmer_none.py
new file mode 100644
index 0000000..7249036
--- /dev/null
+++ b/app/www/library/utilities/shimmer_none.py
@@ -0,0 +1,9 @@
+import reflex as rx
+
+
+def shimmer_none():
+ return rx.el.div(
+ rx.el.p("Generating response...", class_name="shimmer md:shimmer-none"),
+ rx.el.p("shimmer md:shimmer-none", class_name="font-mono text-xs"),
+ class_name="flex flex-col items-center gap-3 text-sm text-muted-foreground",
+ )
diff --git a/app/www/library/utilities/shimmer_play_once.py b/app/www/library/utilities/shimmer_play_once.py
new file mode 100644
index 0000000..83de1cb
--- /dev/null
+++ b/app/www/library/utilities/shimmer_play_once.py
@@ -0,0 +1,28 @@
+import reflex as rx
+from reflex.experimental import ClientStateVar
+
+from components.ui.button import button
+
+shimmer_key = ClientStateVar.create("shimmer_key", 0)
+
+
+def shimmer_once():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.p(
+ "Generating response...",
+ class_name=(
+ "shimmer text-sm text-muted-foreground "
+ "shimmer-duration-1100 shimmer-once"
+ ),
+ ),
+ key=shimmer_key.value.to(str),
+ ),
+ button(
+ "Replay",
+ variant="outline",
+ size="sm",
+ on_click=shimmer_key.set_value(shimmer_key.value.to(int) + 1),
+ ),
+ class_name="flex flex-col items-center gap-4",
+ )
diff --git a/app/www/library/utilities/shimmer_reverse.py b/app/www/library/utilities/shimmer_reverse.py
new file mode 100644
index 0000000..c1b904a
--- /dev/null
+++ b/app/www/library/utilities/shimmer_reverse.py
@@ -0,0 +1,12 @@
+import reflex as rx
+
+
+def shimmer_reverse():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.p("Generating response...", class_name="shimmer shimmer-reverse"),
+ rx.el.p("shimmer reverse", class_name="font-mono text-xs"),
+ class_name="flex flex-col gap-3",
+ ),
+ class_name="flex text-center items-center gap-2 text-sm text-muted-foreground",
+ )
diff --git a/app/www/library/utilities/shimmer_spread.py b/app/www/library/utilities/shimmer_spread.py
new file mode 100644
index 0000000..2a8cf6f
--- /dev/null
+++ b/app/www/library/utilities/shimmer_spread.py
@@ -0,0 +1,17 @@
+import reflex as rx
+
+
+def shimmer_spread():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.p("Generating response...", class_name="shimmer shimmer-spread-4"),
+ rx.el.p("shimmer-spread-4", class_name="font-mono text-xs"),
+ class_name="flex flex-col gap-3",
+ ),
+ rx.el.div(
+ rx.el.p("Generating response...", class_name="shimmer shimmer-spread-24"),
+ rx.el.p("shimmer-spread-24", class_name="font-mono text-xs"),
+ class_name="flex flex-col gap-3",
+ ),
+ class_name="mx-auto grid w-full max-w-lg gap-6 text-center text-sm text-muted-foreground sm:grid-cols-2",
+ )
diff --git a/assets/docs/utilities/scroll-fade.md b/assets/docs/utilities/scroll-fade.md
new file mode 100644
index 0000000..b49ccab
--- /dev/null
+++ b/assets/docs/utilities/scroll-fade.md
@@ -0,0 +1,295 @@
+
+
+# Scroll Fade
+
+Utilities for adding a fade effect to the edges of a scroll container.
+
+>The **scroll-fade** utility is purely composed of CSS and is based on [shadcn/scroll-fade](https://ui.shadcn.com/docs/utils/scroll-fade). No extensions to **rxconfig.py** are needed as it uses **Tailwind v4** syntax.
+
+# Installation
+
+If your project was set up with `buridan init`, you already have scroll-fade. It ships with the `buridan` package, which the CLI imports in your global CSS file.
+
+Otherwise install the `buirdan` package:
+
+```uv
+uv run buridan init
+```
+
+# Usage
+
+| Class | Styles |
+| --------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
+| `scroll-fade` | `mask-image: var(--scroll-fade-mask, var(--scroll-fade-block));`
`animation-timeline: scroll(self y);` |
+| `scroll-fade-y` | `mask-image: var(--scroll-fade-mask, var(--scroll-fade-block));`
`animation-timeline: scroll(self y);` |
+| `scroll-fade-x` | `mask-image: var(--scroll-fade-mask, var(--scroll-fade-inline));`
`animation-timeline: scroll(self inline);` |
+| `scroll-fade-t` | Fade mask on the top edge.
`animation-timeline: scroll(self y);` |
+| `scroll-fade-b` | Fade mask on the bottom edge.
`animation-timeline: scroll(self y);` |
+| `scroll-fade-l` | Fade mask on the left edge.
`animation-timeline: scroll(self x);` |
+| `scroll-fade-r` | Fade mask on the right edge.
`animation-timeline: scroll(self x);` |
+| `scroll-fade-s` | Fade mask on the start edge, mirrors in RTL.
`animation-timeline: scroll(self inline);` |
+| `scroll-fade-e` | Fade mask on the end edge, mirrors in RTL.
`animation-timeline: scroll(self inline);` |
+| `scroll-fade-` | `--scroll-fade-size: calc(var(--spacing) * );` |
+| `scroll-fade-[]` | `--scroll-fade-size: ;` |
+| `scroll-fade-{t,b,s,e}-` | `--scroll-fade-{t,b,s,e}-size: calc(var(--spacing) * );` |
+| `scroll-fade-{t,b,s,e}-[]` | `--scroll-fade-{t,b,s,e}-size: ;` |
+| `scroll-fade-none` | `--scroll-fade-mask: none;` |
+
+Add `scroll-fade` or `scroll-fade-y` to the scroll container, i.e. the element that has overflow-y-auto.
+
+The fade is scroll-aware and tracks the scroll position:
+
+- At rest, the top edge is crisp and the bottom edge fades to hint at more content.
+- As you scroll, a fade appears at the top and both edges stay faded mid-scroll.
+- At the end, the bottom edge sharpens to show you have reached the last item.
+
+The fade is applied with `mask-image`, so it dissolves the content itself rather than overlaying a color. The mask uses a linear fade from transparent to black, so it adapts to any background without configuration. If your scroll area sits inside a card, put the background and border on a wrapper and `scroll-fade` on the inner scroller, so the fade dissolves the content and not the card.
+
+
+# Scroll Fade Demo
+
+
+```python
+def scroll_fade_demo():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ *[
+ rx.el.div(
+ f"Item {index + 1}",
+ class_name="rounded-lg bg-muted px-3 py-2.5 text-sm",
+ )
+ for index in range(12)
+ ],
+ class_name="flex flex-col gap-1.5 p-1.5",
+ ),
+ class_name="h-72 scroll-fade scrollbar-none overflow-y-auto",
+ ),
+ class_name="mx-auto w-full max-w-xs overflow-hidden rounded-2xl border border-input",
+ )
+```
+
+
+# No Overflow, No Fade
+
+If the content does not overflow, no fade is shown. You can apply `scroll-fade` to any list without checking whether it scrolls.
+
+
+```python
+def scroll_fade_no_fade():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ *[
+ rx.el.div(
+ f"Item {index + 1}",
+ class_name="rounded-lg bg-muted px-3 py-2.5 text-sm",
+ )
+ for index in range(3)
+ ],
+ class_name="flex flex-col gap-1.5 p-1.5",
+ ),
+ class_name="scroll-fade scrollbar-none overflow-y-auto",
+ ),
+ class_name="mx-auto w-full max-w-xs overflow-hidden rounded-2xl border border-input",
+ )
+```
+
+
+# Horizontal Scrolling
+
+Use `scroll-fade-x` on containers that scroll horizontally, i.e. the element that has `overflow-x-auto`.
+
+
+```python
+def scroll_fade_horizontal():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ *[
+ rx.el.div(
+ tag,
+ class_name="shrink-0 rounded-lg bg-muted px-3 py-2.5 text-sm",
+ )
+ for tag in tags
+ ],
+ class_name="flex w-max gap-1.5 p-1.5",
+ ),
+ class_name="scroll-fade-x scrollbar-none overflow-x-auto",
+ ),
+ class_name="mx-auto w-full max-w-xs overflow-hidden rounded-2xl border border-input",
+ )
+```
+
+
+# Edge Fades
+
+Use edge utilities when only one edge should track the scroll position.
+
+
+```python
+def scroll_fade_edge():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ scroll_fade_edge_items(),
+ class_name="h-36 scroll-fade-t scrollbar-none overflow-y-auto",
+ ),
+ class_name="overflow-hidden rounded-2xl border border-input",
+ ),
+ rx.el.p(
+ "scroll-fade-t",
+ class_name="text-center font-mono text-xs text-muted-foreground",
+ ),
+ class_name="flex flex-col gap-3",
+ ),
+ rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ scroll_fade_edge_items(),
+ class_name="h-36 scroll-fade-b scrollbar-none overflow-y-auto",
+ ),
+ class_name="overflow-hidden rounded-2xl border border-input",
+ ),
+ rx.el.p(
+ "scroll-fade-b",
+ class_name="text-center font-mono text-xs text-muted-foreground",
+ ),
+ class_name="flex flex-col gap-3",
+ ),
+ rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ scroll_fade_edge_tags(),
+ class_name="scroll-fade-s scrollbar-none overflow-x-auto",
+ ),
+ class_name="overflow-hidden rounded-2xl border border-input",
+ ),
+ rx.el.p(
+ "scroll-fade-s",
+ class_name="text-center font-mono text-xs text-muted-foreground",
+ ),
+ class_name="flex flex-col gap-3",
+ ),
+ rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ scroll_fade_edge_tags(),
+ class_name="scroll-fade-e scrollbar-none overflow-x-auto",
+ ),
+ class_name="overflow-hidden rounded-2xl border border-input",
+ ),
+ rx.el.p(
+ "scroll-fade-e",
+ class_name="text-center font-mono text-xs text-muted-foreground",
+ ),
+ class_name="flex flex-col gap-3",
+ ),
+ class_name="mx-auto flex max-w-xs min-w-0 flex-col gap-6",
+ )
+```
+
+
+The edge utilities are scroll-aware. Start edges fade in after you scroll away from the start, and end edges fade out when you reach the end. Use `scroll-fade-t`, `scroll-fade-b`, `scroll-fade-l`, and `scroll-fade-r` for physical edges. Use `scroll-fade-s` and `scroll-fade-e` for logical inline edges.
+
+# Fade Size
+
+The fade depth defaults to 12% of the container, capped at 40px so tall scrollers stay subtle. Use `scroll-fade-` to set a fixed size on the spacing scale instead, the same way `scroll-mt-` works.
+
+
+```python
+def scroll_fade_size():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ scroll_fade_size_items(),
+ class_name="h-48 scroll-fade scrollbar-none overflow-y-auto scroll-fade-4",
+ ),
+ class_name="overflow-hidden rounded-2xl border border-input",
+ ),
+ rx.el.p(
+ "scroll-fade-4",
+ class_name="text-center font-mono text-xs text-muted-foreground",
+ ),
+ class_name="flex flex-col gap-3",
+ ),
+ rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ scroll_fade_size_items(),
+ class_name="h-48 scroll-fade scrollbar-none overflow-y-auto scroll-fade-24",
+ ),
+ class_name="overflow-hidden rounded-2xl border border-input",
+ ),
+ rx.el.p(
+ "scroll-fade-24",
+ class_name="text-center font-mono text-xs text-muted-foreground",
+ ),
+ class_name="flex flex-col gap-3",
+ ),
+ class_name="mx-auto flex w-full max-w-xs flex-col gap-6",
+ )
+```
+
+
+For one-off values, use an arbitrary length or percentage:
+
+```reflex
+rx.el.div(..., class_name="scroll-fade overflow-y-auto scroll-fade-[15%]")
+```
+
+To fade opposite edges by different amounts, use the per-edge modifiers `scroll-fade-t-`, `scroll-fade-b-`, `scroll-fade-s-`, and `scroll-fade-e-`. They override scroll-fade- on the edge they target and accept arbitrary values too.
+
+```reflex
+rx.el.div(..., class_name="scroll-fade overflow-y-auto scroll-fade-b-8 scroll-fade-t-2")
+```
+
+# Disabling the Fade
+
+Use `scroll-fade-none` to remove the fade. It works in any class order, so the typical use is responsive or stateful.
+
+
+```python
+def scroll_fade_none():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ scroll_fade_none_items(),
+ class_name="h-48 scroll-fade scrollbar-none overflow-y-auto",
+ ),
+ class_name="overflow-hidden rounded-2xl border border-input",
+ ),
+ rx.el.p(
+ "scroll-fade",
+ class_name="text-center font-mono text-xs text-muted-foreground",
+ ),
+ class_name="flex flex-col gap-3",
+ ),
+ rx.el.div(
+ rx.el.div(
+ rx.el.div(
+ scroll_fade_none_items(),
+ class_name="h-48 scroll-fade scrollbar-none overflow-y-auto scroll-fade-none",
+ ),
+ class_name="overflow-hidden rounded-2xl border border-input",
+ ),
+ rx.el.p(
+ "scroll-fade scroll-fade-none",
+ class_name="text-center font-mono text-xs text-muted-foreground",
+ ),
+ class_name="flex flex-col gap-3",
+ ),
+ class_name="mx-auto flex w-full max-w-xs min-w-0 flex-col gap-6",
+ )
+```
+
+
+
+# Fallback
+
+The scroll-aware behavior is implemented with [CSS scroll-driven animations](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Scroll-driven_animations), with no JavaScript and no scroll listeners. In browsers that do not support scroll-driven animations, `scroll-fade` falls back to a static fade on both edges, and edge utilities fall back to a static fade on the selected edge.
+
+Since the mask is applied to the scroll container itself, a visible scrollbar fades with the content at the edges. Pair `scroll-fade` with `no-scrollbar`, which ships in the same package, if you want to hide the scrollbar entirely.
diff --git a/assets/docs/utilities/shimmer.md b/assets/docs/utilities/shimmer.md
new file mode 100644
index 0000000..7088671
--- /dev/null
+++ b/assets/docs/utilities/shimmer.md
@@ -0,0 +1,317 @@
+
+
+# Shimmer
+
+Utilities for adding a shimmer effect to text elements.
+
+>The **shimmer** utility is purely composed of CSS and is based on [shadcn/shimmer](https://ui.shadcn.com/docs/utils/shimmer). No extensions to **rxconfig.py** are needed as it uses **Tailwind v4** syntax.
+
+
+# Installation
+
+If your project was set up with `buridan init`, you already have shimmer. It ships with the `buridan` package, which the CLI imports in your global CSS file.
+
+Otherwise install the `buirdan` package:
+
+```uv
+uv run buridan init
+```
+
+You can also copy paste the `shimmer` source directly into your `globals.css` file. Make sure you also include the correct imports, such as `@tailwind utilities;` at the top of your CSS file.
+
+```css
+@property --shimmer-angle {
+ syntax: "";
+ inherits: true;
+ initial-value: 20deg;
+}
+@property --shimmer-image {
+ syntax: "*";
+ inherits: false;
+}
+@property --shimmer-text-fill {
+ syntax: "*";
+ inherits: false;
+}
+
+@theme inline {
+ @keyframes tw-shimmer {
+ from {
+ background-position: 100% 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+ }
+}
+
+@utility shimmer {
+ --_spread: var(--shimmer-spread, calc(3ch + 40px));
+ --_base: currentColor;
+ --_highlight: var(
+ --shimmer-color,
+ oklch(from currentColor l c h / calc(alpha* 0.2))
+ );
+
+ background-image: var(
+ --shimmer-image,
+ linear-gradient(
+ calc(90deg + var(--shimmer-angle)),
+ var(--_base) calc(50% - var(--_spread)),
+ color-mix(in oklch, var(--_highlight), var(--_base) 50%)
+ calc(50% - var(--_spread) * 0.5),
+ var(--_highlight) 50%,
+ color-mix(in oklch, var(--_highlight), var(--_base) 50%)
+ calc(50% + var(--_spread) * 0.5),
+ var(--_base) calc(50% + var(--_spread))
+ )
+ );
+ background-repeat: no-repeat;
+ background-size: calc(200% + var(--_spread) * 2) 100%;
+ background-position: 0 0;
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: var(--shimmer-text-fill, transparent);
+ animation: tw-shimmer var(--shimmer-duration, 2s) linear infinite;
+
+ @variant dark {
+ --_highlight: var(
+ --shimmer-color,
+ oklch(
+ from currentColor max(0.8, calc(l + 0.4)) c h /
+ calc(alpha + 0.4)
+ )
+ );
+ }
+
+ &:where([dir="rtl"], [dir="rtl"] *) {
+ animation-direction: reverse;
+ }
+}
+
+@utility shimmer-once {
+ animation-iteration-count: 1;
+}
+
+@utility shimmer-reverse {
+ animation-direction: reverse;
+}
+
+@utility shimmer-none {
+ --shimmer-image: none;
+ --shimmer-text-fill: currentColor;
+}
+
+@utility shimmer-color-* {
+ --shimmer-color: --value(--color, [color]);
+ --shimmer-color: color-mix(
+ in oklch,
+ --value(--color, [color]) calc(--modifier(integer) * 1%),
+ transparent
+ );
+}
+
+@utility shimmer-duration-* {
+ --shimmer-duration: calc(--value(integer) * 1ms);
+}
+
+@utility shimmer-spread-* {
+ --shimmer-spread: calc(var(--spacing) * --value(integer));
+ --shimmer-spread: --value([length], [percentage]);
+}
+
+@utility shimmer-angle-* {
+ --shimmer-angle: calc(--value(integer) * 1deg);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .shimmer {
+ animation: none;
+ background-image: none;
+ -webkit-text-fill-color: currentColor;
+ }
+}
+```
+
+
+# Usage
+
+| Class | Styles |
+| ----------------------------- | ---------------------------------------------------------------------------------------------------- |
+| `shimmer` | `background-clip: text;`
`animation: tw-shimmer var(--shimmer-duration, 2s) linear infinite;` |
+| `shimmer-once` | `animation-iteration-count: 1;` |
+| `shimmer-reverse` | `animation-direction: reverse;` |
+| `shimmer-none` | `--shimmer-image: none;`
`--shimmer-text-fill: currentColor;` |
+| `shimmer-color-` | `--shimmer-color: ;` |
+| `shimmer-color-[]` | `--shimmer-color: ;` |
+| `shimmer-color-/` | `--shimmer-color: color-mix(in oklch, , transparent);` |
+| `shimmer-duration-` | `--shimmer-duration: calc( * 1ms);` |
+| `shimmer-spread-` | `--shimmer-spread: calc(var(--spacing) * );` |
+| `shimmer-spread-[]` | `--shimmer-spread: ;` |
+| `shimmer-angle-` | `--shimmer-angle: calc( * 1deg);` |
+
+Add shimmer to a text element.
+
+```reflex
+rx.el.p("Generating response", class_name="shimmer text-muted-foreground w-fit")
+```
+
+The effect is pure CSS. The text is painted with `background-clip: text`, and the highlight sweeps across it in a seamless loop.
+
+>**Note**: If the **shimmer** utility isn't working properly, try adding **w-fit** to the text styling, as it ensures the animation doesn't exceed the inherit width of the text.
+
+# Color
+
+Use `shimmer-color-` to set the highlight color explicitly. It accepts theme colors with an optional opacity modifier, or any arbitrary color value.
+
+
+```python
+def shimmer_color():
+ return rx.el.div(
+ rx.el.p(
+ "Generating response", class_name="shimmer shimmer-color-blue-500/60 w-fit"
+ ),
+ rx.el.p(
+ "Generating response", class_name="shimmer shimmer-color-[#378ADD] w-fit"
+ ),
+ class_name="flex flex-col items-center gap-2 text-sm text-muted-foreground",
+ )
+```
+
+
+# Duration
+
+Use `shimmer-duration-` to set the duration of one sweep in milliseconds. The default is `2000`, i.e. `2s`.
+
+
+```python
+def shimmer_duration():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.p("Generating response...", class_name="shimmer"),
+ rx.el.p("shimmer", class_name="font-mono text-xs"),
+ class_name="flex flex-col gap-3",
+ ),
+ rx.el.div(
+ rx.el.p(
+ "Generating response...", class_name="shimmer shimmer-duration-1000"
+ ),
+ rx.el.p("shimmer-duration-1000", class_name="font-mono text-xs"),
+ class_name="flex flex-col gap-3",
+ ),
+ class_name="mx-auto grid w-full max-w-lg gap-6 text-center text-sm text-muted-foreground sm:grid-cols-2",
+ )
+```
+
+
+# Spread
+
+Use `shimmer-spread-` to set the width of the highlight band using the spacing scale. The default is `calc(3ch + 40px)`: a fixed base plus a `3ch` term that scales with the font size.
+
+
+```python
+def shimmer_spread():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.p("Generating response...", class_name="shimmer shimmer-spread-4"),
+ rx.el.p("shimmer-spread-4", class_name="font-mono text-xs"),
+ class_name="flex flex-col gap-3",
+ ),
+ rx.el.div(
+ rx.el.p("Generating response...", class_name="shimmer shimmer-spread-24"),
+ rx.el.p("shimmer-spread-24", class_name="font-mono text-xs"),
+ class_name="flex flex-col gap-3",
+ ),
+ class_name="mx-auto grid w-full max-w-lg gap-6 text-center text-sm text-muted-foreground sm:grid-cols-2",
+ )
+```
+
+
+# Angle
+
+Use `shimmer-angle-` to set the tilt of the highlight band in degrees. The default is `20`.
+
+
+```python
+def shimmer_angle():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.p("Generating response...", class_name="shimmer"),
+ rx.el.p("shimmer", class_name="font-mono text-xs"),
+ class_name="flex flex-col gap-3",
+ ),
+ rx.el.div(
+ rx.el.p("Generating response...", class_name="shimmer shimmer-angle-45"),
+ rx.el.p("shimmer-angle-45", class_name="font-mono text-xs"),
+ class_name="flex flex-col gap-3",
+ ),
+ class_name="mx-auto grid w-full max-w-lg gap-6 text-center text-sm text-muted-foreground sm:grid-cols-2",
+ )
+```
+
+
+# Reverse
+
+Use `shimmer-reverse` to sweep the highlight in the opposite direction.
+
+
+```python
+def shimmer_reverse():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.p("Generating response...", class_name="shimmer shimmer-reverse"),
+ rx.el.p("shimmer reverse", class_name="font-mono text-xs"),
+ class_name="flex flex-col gap-3",
+ ),
+ class_name="flex text-center items-center gap-2 text-sm text-muted-foreground",
+ )
+```
+
+
+# Play Once
+
+Use `shimmer-once` to play a single sweep instead of looping, useful as a reveal when streaming completes. Pair it with `shimmer-duration-` to control how long the sweep takes.
+
+
+```python
+def shimmer_once():
+ return rx.el.div(
+ rx.el.div(
+ rx.el.p(
+ "Generating response...",
+ class_name=(
+ "shimmer text-sm text-muted-foreground "
+ "shimmer-duration-1100 shimmer-once"
+ ),
+ ),
+ key=shimmer_key.value.to(str),
+ ),
+ button(
+ "Replay",
+ variant="outline",
+ size="sm",
+ on_click=shimmer_key.set_value(shimmer_key.value.to(int) + 1),
+ ),
+ class_name="flex flex-col items-center gap-4",
+ )
+```
+
+
+```reflex
+rx.el.p("Response Generated", class_name="shimmer shimmer-duration-1100 shimmer-once")
+```
+
+# Disabling the Shimmer
+
+Use `shimmer-none` to turn the effect off and render the text normally. It works in any class order, so the typical use is responsive or stateful.
+
+
+```python
+def shimmer_none():
+ return rx.el.div(
+ rx.el.p("Generating response...", class_name="shimmer md:shimmer-none"),
+ rx.el.p("shimmer md:shimmer-none", class_name="font-mono text-xs"),
+ class_name="flex flex-col items-center gap-3 text-sm text-muted-foreground",
+ )
+```
+
diff --git a/assets/fuse/searchList.json b/assets/fuse/searchList.json
index e76db1a..9fb1de1 100644
--- a/assets/fuse/searchList.json
+++ b/assets/fuse/searchList.json
@@ -39,6 +39,16 @@
"title": "llms.txt",
"url": "llms.txt"
},
+ {
+ "section": "Utilities",
+ "title": "Scroll Fade",
+ "url": "docs/utilities/scroll-fade"
+ },
+ {
+ "section": "Utilities",
+ "title": "Shimmer",
+ "url": "docs/utilities/shimmer"
+ },
{
"section": "Resources",
"title": "Client State Var",
diff --git a/assets/globals.css b/assets/globals.css
index 5629513..9f21ccf 100644
--- a/assets/globals.css
+++ b/assets/globals.css
@@ -2,6 +2,15 @@
@tailwind components;
@tailwind utilities;
+::selection {
+ background: var(--foreground);
+ color: var(--background);
+}
+
+body {
+ background-color: var(--background);
+}
+
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
@@ -74,16 +83,577 @@
--token-keyword: oklch(0.58 0.22 27);
}
-::selection {
- background: var(--foreground);
- color: var(--background);
+.toc-link[data-active="true"] {
+ color: var(--foreground);
+ font-weight: 500;
}
-body {
- background-color: var(--background);
+/* ── Shimmer utility ────────────────────────────────────────────────────────
+ *
+ * Usage (add any of these to class_name):
+ *
+ * shimmer — base shimmer effect
+ * shimmer-once — play once instead of looping
+ * shimmer-reverse — sweep in opposite direction
+ * shimmer-none — disable the effect
+ * shimmer-color- — e.g. shimmer-color-blue-500
+ * shimmer-duration- — duration in ms, e.g. shimmer-duration-1000
+ * shimmer-spread- — highlight band width, e.g. shimmer-spread-24
+ * shimmer-angle- — tilt in degrees, e.g. shimmer-angle-45
+ *
+ * Add this file to your global CSS with:
+ * @import "./shimmer.css";
+ * or copy the contents directly into your globals.css.
+ * ────────────────────────────────────────────────────────────────────────── */
+
+@property --shimmer-angle {
+ syntax: "";
+ inherits: true;
+ initial-value: 20deg;
+}
+@property --shimmer-image {
+ syntax: "*";
+ inherits: false;
+}
+@property --shimmer-text-fill {
+ syntax: "*";
+ inherits: false;
}
-.toc-link[data-active="true"] {
- color: var(--foreground);
- font-weight: 500;
+@theme inline {
+ @keyframes tw-shimmer {
+ from {
+ background-position: 100% 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+ }
+}
+
+@utility shimmer {
+ --_spread: var(--shimmer-spread, calc(3ch + 40px));
+ --_base: currentColor;
+ --_highlight: var(
+ --shimmer-color,
+ oklch(from currentColor l c h / calc(alpha* 0.2))
+ );
+
+ background-image: var(
+ --shimmer-image,
+ linear-gradient(
+ calc(90deg + var(--shimmer-angle)),
+ var(--_base) calc(50% - var(--_spread)),
+ color-mix(in oklch, var(--_highlight), var(--_base) 50%)
+ calc(50% - var(--_spread) * 0.5),
+ var(--_highlight) 50%,
+ color-mix(in oklch, var(--_highlight), var(--_base) 50%)
+ calc(50% + var(--_spread) * 0.5),
+ var(--_base) calc(50% + var(--_spread))
+ )
+ );
+ background-repeat: no-repeat;
+ background-size: calc(200% + var(--_spread) * 2) 100%;
+ background-position: 0 0;
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: var(--shimmer-text-fill, transparent);
+ animation: tw-shimmer var(--shimmer-duration, 2s) linear infinite;
+
+ @variant dark {
+ --_highlight: var(
+ --shimmer-color,
+ oklch(
+ from currentColor max(0.8, calc(l + 0.4)) c h /
+ calc(alpha + 0.4)
+ )
+ );
+ }
+
+ &:where([dir="rtl"], [dir="rtl"] *) {
+ animation-direction: reverse;
+ }
+}
+
+@utility shimmer-once {
+ animation-iteration-count: 1;
+}
+
+@utility shimmer-reverse {
+ animation-direction: reverse;
+}
+
+@utility shimmer-none {
+ --shimmer-image: none;
+ --shimmer-text-fill: currentColor;
+}
+
+@utility shimmer-color-* {
+ --shimmer-color: --value(--color, [color]);
+ --shimmer-color: color-mix(
+ in oklch,
+ --value(--color, [color]) calc(--modifier(integer) * 1%),
+ transparent
+ );
+}
+
+@utility shimmer-duration-* {
+ --shimmer-duration: calc(--value(integer) * 1ms);
+}
+
+@utility shimmer-spread-* {
+ --shimmer-spread: calc(var(--spacing) * --value(integer));
+ --shimmer-spread: --value([length], [percentage]);
+}
+
+@utility shimmer-angle-* {
+ --shimmer-angle: calc(--value(integer) * 1deg);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .shimmer {
+ animation: none;
+ background-image: none;
+ -webkit-text-fill-color: currentColor;
+ }
+}
+
+/*-------------- SCROLL FADE ----------------------------*/
+/* scroll-fade */
+@property --scroll-fade-t {
+ syntax: "";
+ inherits: false;
+ initial-value: 0px;
+}
+@property --scroll-fade-b {
+ syntax: "";
+ inherits: false;
+ initial-value: 0px;
+}
+@property --scroll-fade-s {
+ syntax: "";
+ inherits: false;
+ initial-value: 0px;
+}
+@property --scroll-fade-e {
+ syntax: "";
+ inherits: false;
+ initial-value: 0px;
+}
+@property --scroll-fade-mask {
+ syntax: "*";
+ inherits: false;
+}
+
+@theme inline {
+ @keyframes scroll-fade-reveal-t {
+ from {
+ --scroll-fade-t: 0px;
+ }
+ to {
+ --scroll-fade-t: var(
+ --_scroll-fade-size-t,
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
+ );
+ }
+ }
+ @keyframes scroll-fade-reveal-b {
+ from {
+ --scroll-fade-b: var(
+ --_scroll-fade-size-b,
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
+ );
+ }
+ to {
+ --scroll-fade-b: 0px;
+ }
+ }
+ @keyframes scroll-fade-reveal-s {
+ from {
+ --scroll-fade-s: 0px;
+ }
+ to {
+ --scroll-fade-s: var(
+ --_scroll-fade-size-s,
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
+ );
+ }
+ }
+ @keyframes scroll-fade-reveal-e {
+ from {
+ --scroll-fade-e: var(
+ --_scroll-fade-size-e,
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
+ );
+ }
+ to {
+ --scroll-fade-e: 0px;
+ }
+ }
+}
+
+@utility scroll-fade {
+ --_scroll-fade-size-t: var(
+ --scroll-fade-t-size,
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
+ );
+ --_scroll-fade-size-b: var(
+ --scroll-fade-b-size,
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
+ );
+ --scroll-fade-block: linear-gradient(
+ to bottom,
+ transparent 0,
+ #000 var(--scroll-fade-t, 0px),
+ #000 calc(100% - var(--scroll-fade-b, 0px)),
+ transparent 100%
+ );
+ -webkit-mask-image: var(--scroll-fade-mask, var(--scroll-fade-block));
+ mask-image: var(--scroll-fade-mask, var(--scroll-fade-block));
+ -webkit-mask-composite: source-in;
+ mask-composite: intersect;
+ -webkit-mask-repeat: no-repeat;
+ mask-repeat: no-repeat;
+
+ @supports (animation-timeline: scroll()) {
+ animation:
+ scroll-fade-reveal-t 1ms ease-in-out,
+ scroll-fade-reveal-b 1ms ease-in-out;
+ animation-timeline: scroll(self y), scroll(self y);
+ animation-range:
+ 0 var(--scroll-fade-reveal, calc(var(--spacing) * 24)),
+ calc(100% - var(--scroll-fade-reveal, calc(var(--spacing) * 24)))
+ 100%;
+ animation-fill-mode: both;
+ }
+
+ @supports not (animation-timeline: scroll()) {
+ --scroll-fade-t: var(--_scroll-fade-size-t);
+ --scroll-fade-b: var(--_scroll-fade-size-b);
+ }
+}
+
+@utility scroll-fade-y {
+ --_scroll-fade-size-t: var(
+ --scroll-fade-t-size,
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
+ );
+ --_scroll-fade-size-b: var(
+ --scroll-fade-b-size,
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
+ );
+ --scroll-fade-block: linear-gradient(
+ to bottom,
+ transparent 0,
+ #000 var(--scroll-fade-t, 0px),
+ #000 calc(100% - var(--scroll-fade-b, 0px)),
+ transparent 100%
+ );
+ -webkit-mask-image: var(--scroll-fade-mask, var(--scroll-fade-block));
+ mask-image: var(--scroll-fade-mask, var(--scroll-fade-block));
+ -webkit-mask-composite: source-in;
+ mask-composite: intersect;
+ -webkit-mask-repeat: no-repeat;
+ mask-repeat: no-repeat;
+
+ @supports (animation-timeline: scroll()) {
+ animation:
+ scroll-fade-reveal-t 1ms ease-in-out,
+ scroll-fade-reveal-b 1ms ease-in-out;
+ animation-timeline: scroll(self y), scroll(self y);
+ animation-range:
+ 0 var(--scroll-fade-reveal, calc(var(--spacing) * 24)),
+ calc(100% - var(--scroll-fade-reveal, calc(var(--spacing) * 24)))
+ 100%;
+ animation-fill-mode: both;
+ }
+
+ @supports not (animation-timeline: scroll()) {
+ --scroll-fade-t: var(--_scroll-fade-size-t);
+ --scroll-fade-b: var(--_scroll-fade-size-b);
+ }
+}
+
+@utility scroll-fade-x {
+ --_scroll-fade-size-s: var(
+ --scroll-fade-s-size,
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
+ );
+ --_scroll-fade-size-e: var(
+ --scroll-fade-e-size,
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
+ );
+ --scroll-fade-inline: linear-gradient(
+ to right,
+ transparent 0,
+ #000 var(--scroll-fade-s, 0px),
+ #000 calc(100% - var(--scroll-fade-e, 0px)),
+ transparent 100%
+ );
+ &:where([dir="rtl"], [dir="rtl"] *) {
+ --scroll-fade-inline: linear-gradient(
+ to left,
+ transparent 0,
+ #000 var(--scroll-fade-s, 0px),
+ #000 calc(100% - var(--scroll-fade-e, 0px)),
+ transparent 100%
+ );
+ }
+ -webkit-mask-image: var(--scroll-fade-mask, var(--scroll-fade-inline));
+ mask-image: var(--scroll-fade-mask, var(--scroll-fade-inline));
+ -webkit-mask-composite: source-in;
+ mask-composite: intersect;
+ -webkit-mask-repeat: no-repeat;
+ mask-repeat: no-repeat;
+
+ @supports (animation-timeline: scroll()) {
+ animation:
+ scroll-fade-reveal-s 1ms ease-in-out,
+ scroll-fade-reveal-e 1ms ease-in-out;
+ animation-timeline: scroll(self inline), scroll(self inline);
+ animation-range:
+ 0 var(--scroll-fade-reveal, calc(var(--spacing) * 24)),
+ calc(100% - var(--scroll-fade-reveal, calc(var(--spacing) * 24)))
+ 100%;
+ animation-fill-mode: both;
+ }
+
+ @supports not (animation-timeline: scroll()) {
+ --scroll-fade-s: var(--_scroll-fade-size-s);
+ --scroll-fade-e: var(--_scroll-fade-size-e);
+ }
+}
+
+@utility scroll-fade-t {
+ --_scroll-fade-size-t: var(
+ --scroll-fade-t-size,
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
+ );
+ --scroll-fade-mask: linear-gradient(
+ to bottom,
+ transparent 0,
+ #000 var(--scroll-fade-t, 0px),
+ #000 100%
+ );
+ -webkit-mask-image: var(--scroll-fade-mask);
+ mask-image: var(--scroll-fade-mask);
+ -webkit-mask-composite: source-in;
+ mask-composite: intersect;
+ -webkit-mask-repeat: no-repeat;
+ mask-repeat: no-repeat;
+
+ @supports (animation-timeline: scroll()) {
+ animation: scroll-fade-reveal-t 1ms ease-in-out;
+ animation-timeline: scroll(self y);
+ animation-range: 0 var(--scroll-fade-reveal, calc(var(--spacing) * 24));
+ animation-fill-mode: both;
+ }
+
+ @supports not (animation-timeline: scroll()) {
+ --scroll-fade-t: var(--_scroll-fade-size-t);
+ }
+}
+
+@utility scroll-fade-b {
+ --_scroll-fade-size-b: var(
+ --scroll-fade-b-size,
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
+ );
+ --scroll-fade-mask: linear-gradient(
+ to bottom,
+ #000 0,
+ #000 calc(100% - var(--scroll-fade-b, 0px)),
+ transparent 100%
+ );
+ -webkit-mask-image: var(--scroll-fade-mask);
+ mask-image: var(--scroll-fade-mask);
+ -webkit-mask-composite: source-in;
+ mask-composite: intersect;
+ -webkit-mask-repeat: no-repeat;
+ mask-repeat: no-repeat;
+
+ @supports (animation-timeline: scroll()) {
+ animation: scroll-fade-reveal-b 1ms ease-in-out;
+ animation-timeline: scroll(self y);
+ animation-range: calc(
+ 100% - var(--scroll-fade-reveal, calc(var(--spacing) * 24))
+ )
+ 100%;
+ animation-fill-mode: both;
+ }
+
+ @supports not (animation-timeline: scroll()) {
+ --scroll-fade-b: var(--_scroll-fade-size-b);
+ }
+}
+
+@utility scroll-fade-l {
+ --_scroll-fade-size-s: var(
+ --scroll-fade-s-size,
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
+ );
+ --scroll-fade-mask: linear-gradient(
+ to right,
+ transparent 0,
+ #000 var(--scroll-fade-s, 0px),
+ #000 100%
+ );
+ -webkit-mask-image: var(--scroll-fade-mask);
+ mask-image: var(--scroll-fade-mask);
+ -webkit-mask-composite: source-in;
+ mask-composite: intersect;
+ -webkit-mask-repeat: no-repeat;
+ mask-repeat: no-repeat;
+
+ @supports (animation-timeline: scroll()) {
+ animation: scroll-fade-reveal-s 1ms ease-in-out;
+ animation-timeline: scroll(self x);
+ animation-range: 0 var(--scroll-fade-reveal, calc(var(--spacing) * 24));
+ animation-fill-mode: both;
+ }
+
+ @supports not (animation-timeline: scroll()) {
+ --scroll-fade-s: var(--_scroll-fade-size-s);
+ }
+}
+
+@utility scroll-fade-r {
+ --_scroll-fade-size-e: var(
+ --scroll-fade-e-size,
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
+ );
+ --scroll-fade-mask: linear-gradient(
+ to right,
+ #000 0,
+ #000 calc(100% - var(--scroll-fade-e, 0px)),
+ transparent 100%
+ );
+ -webkit-mask-image: var(--scroll-fade-mask);
+ mask-image: var(--scroll-fade-mask);
+ -webkit-mask-composite: source-in;
+ mask-composite: intersect;
+ -webkit-mask-repeat: no-repeat;
+ mask-repeat: no-repeat;
+
+ @supports (animation-timeline: scroll()) {
+ animation: scroll-fade-reveal-e 1ms ease-in-out;
+ animation-timeline: scroll(self x);
+ animation-range: calc(
+ 100% - var(--scroll-fade-reveal, calc(var(--spacing) * 24))
+ )
+ 100%;
+ animation-fill-mode: both;
+ }
+
+ @supports not (animation-timeline: scroll()) {
+ --scroll-fade-e: var(--_scroll-fade-size-e);
+ }
+}
+
+@utility scroll-fade-s {
+ --_scroll-fade-size-s: var(
+ --scroll-fade-s-size,
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
+ );
+ --scroll-fade-mask: linear-gradient(
+ to right,
+ transparent 0,
+ #000 var(--scroll-fade-s, 0px),
+ #000 100%
+ );
+ &:where([dir="rtl"], [dir="rtl"] *) {
+ --scroll-fade-mask: linear-gradient(
+ to left,
+ transparent 0,
+ #000 var(--scroll-fade-s, 0px),
+ #000 100%
+ );
+ }
+ -webkit-mask-image: var(--scroll-fade-mask);
+ mask-image: var(--scroll-fade-mask);
+ -webkit-mask-composite: source-in;
+ mask-composite: intersect;
+ -webkit-mask-repeat: no-repeat;
+ mask-repeat: no-repeat;
+
+ @supports (animation-timeline: scroll()) {
+ animation: scroll-fade-reveal-s 1ms ease-in-out;
+ animation-timeline: scroll(self inline);
+ animation-range: 0 var(--scroll-fade-reveal, calc(var(--spacing) * 24));
+ animation-fill-mode: both;
+ }
+
+ @supports not (animation-timeline: scroll()) {
+ --scroll-fade-s: var(--_scroll-fade-size-s);
+ }
+}
+
+@utility scroll-fade-e {
+ --_scroll-fade-size-e: var(
+ --scroll-fade-e-size,
+ var(--scroll-fade-size, min(12%, calc(var(--spacing) * 10)))
+ );
+ --scroll-fade-mask: linear-gradient(
+ to right,
+ #000 0,
+ #000 calc(100% - var(--scroll-fade-e, 0px)),
+ transparent 100%
+ );
+ &:where([dir="rtl"], [dir="rtl"] *) {
+ --scroll-fade-mask: linear-gradient(
+ to left,
+ #000 0,
+ #000 calc(100% - var(--scroll-fade-e, 0px)),
+ transparent 100%
+ );
+ }
+ -webkit-mask-image: var(--scroll-fade-mask);
+ mask-image: var(--scroll-fade-mask);
+ -webkit-mask-composite: source-in;
+ mask-composite: intersect;
+ -webkit-mask-repeat: no-repeat;
+ mask-repeat: no-repeat;
+
+ @supports (animation-timeline: scroll()) {
+ animation: scroll-fade-reveal-e 1ms ease-in-out;
+ animation-timeline: scroll(self inline);
+ animation-range: calc(
+ 100% - var(--scroll-fade-reveal, calc(var(--spacing) * 24))
+ )
+ 100%;
+ animation-fill-mode: both;
+ }
+
+ @supports not (animation-timeline: scroll()) {
+ --scroll-fade-e: var(--_scroll-fade-size-e);
+ }
+}
+
+@utility scroll-fade-* {
+ --scroll-fade-size: calc(var(--spacing) * --value(integer));
+ --scroll-fade-size: --value([length], [percentage]);
+}
+
+@utility scroll-fade-t-* {
+ --scroll-fade-t-size: calc(var(--spacing) * --value(integer));
+ --scroll-fade-t-size: --value([length], [percentage]);
+}
+
+@utility scroll-fade-b-* {
+ --scroll-fade-b-size: calc(var(--spacing) * --value(integer));
+ --scroll-fade-b-size: --value([length], [percentage]);
+}
+
+@utility scroll-fade-s-* {
+ --scroll-fade-s-size: calc(var(--spacing) * --value(integer));
+ --scroll-fade-s-size: --value([length], [percentage]);
+}
+
+@utility scroll-fade-e-* {
+ --scroll-fade-e-size: calc(var(--spacing) * --value(integer));
+ --scroll-fade-e-size: --value([length], [percentage]);
+}
+
+@utility scroll-fade-none {
+ --scroll-fade-mask: none;
}
diff --git a/assets/social/scroll-fade.webp b/assets/social/scroll-fade.webp
new file mode 100644
index 0000000..5c6279c
Binary files /dev/null and b/assets/social/scroll-fade.webp differ
diff --git a/assets/social/shimmer.webp b/assets/social/shimmer.webp
new file mode 100644
index 0000000..f1115a5
Binary files /dev/null and b/assets/social/shimmer.webp differ
diff --git a/docs/utilities/scroll_fade.md b/docs/utilities/scroll_fade.md
new file mode 100644
index 0000000..2d5749e
--- /dev/null
+++ b/docs/utilities/scroll_fade.md
@@ -0,0 +1,106 @@
+---
+title: "Scroll Fade"
+description: "Utilities for adding a fade effect to the edges of a scroll container."
+order: 0
+---
+
+# Scroll Fade
+
+Utilities for adding a fade effect to the edges of a scroll container.
+
+>The **scroll-fade** utility is purely composed of CSS and is based on [shadcn/scroll-fade](https://ui.shadcn.com/docs/utils/scroll-fade). No extensions to **rxconfig.py** are needed as it uses **Tailwind v4** syntax.
+
+# Installation
+
+If your project was set up with `buridan init`, you already have scroll-fade. It ships with the `buridan` package, which the CLI imports in your global CSS file.
+
+Otherwise install the `buirdan` package:
+
+```uv
+uv run buridan init
+```
+
+# Usage
+
+| Class | Styles |
+| --------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
+| `scroll-fade` | `mask-image: var(--scroll-fade-mask, var(--scroll-fade-block));`
`animation-timeline: scroll(self y);` |
+| `scroll-fade-y` | `mask-image: var(--scroll-fade-mask, var(--scroll-fade-block));`
`animation-timeline: scroll(self y);` |
+| `scroll-fade-x` | `mask-image: var(--scroll-fade-mask, var(--scroll-fade-inline));`
`animation-timeline: scroll(self inline);` |
+| `scroll-fade-t` | Fade mask on the top edge.
`animation-timeline: scroll(self y);` |
+| `scroll-fade-b` | Fade mask on the bottom edge.
`animation-timeline: scroll(self y);` |
+| `scroll-fade-l` | Fade mask on the left edge.
`animation-timeline: scroll(self x);` |
+| `scroll-fade-r` | Fade mask on the right edge.
`animation-timeline: scroll(self x);` |
+| `scroll-fade-s` | Fade mask on the start edge, mirrors in RTL.
`animation-timeline: scroll(self inline);` |
+| `scroll-fade-e` | Fade mask on the end edge, mirrors in RTL.
`animation-timeline: scroll(self inline);` |
+| `scroll-fade-` | `--scroll-fade-size: calc(var(--spacing) * );` |
+| `scroll-fade-[]` | `--scroll-fade-size: ;` |
+| `scroll-fade-{t,b,s,e}-` | `--scroll-fade-{t,b,s,e}-size: calc(var(--spacing) * );` |
+| `scroll-fade-{t,b,s,e}-[]` | `--scroll-fade-{t,b,s,e}-size: ;` |
+| `scroll-fade-none` | `--scroll-fade-mask: none;` |
+
+Add `scroll-fade` or `scroll-fade-y` to the scroll container, i.e. the element that has overflow-y-auto.
+
+The fade is scroll-aware and tracks the scroll position:
+
+- At rest, the top edge is crisp and the bottom edge fades to hint at more content.
+- As you scroll, a fade appears at the top and both edges stay faded mid-scroll.
+- At the end, the bottom edge sharpens to show you have reached the last item.
+
+The fade is applied with `mask-image`, so it dissolves the content itself rather than overlaying a color. The mask uses a linear fade from transparent to black, so it adapts to any background without configuration. If your scroll area sits inside a card, put the background and border on a wrapper and `scroll-fade` on the inner scroller, so the fade dissolves the content and not the card.
+
+
+# Scroll Fade Demo
+
+--DEMO(scroll_fade_demo)--
+
+# No Overflow, No Fade
+
+If the content does not overflow, no fade is shown. You can apply `scroll-fade` to any list without checking whether it scrolls.
+
+--DEMO(scroll_fade_no_fade)--
+
+# Horizontal Scrolling
+
+Use `scroll-fade-x` on containers that scroll horizontally, i.e. the element that has `overflow-x-auto`.
+
+--DEMO(scroll_fade_horizontal)--
+
+# Edge Fades
+
+Use edge utilities when only one edge should track the scroll position.
+
+--DEMO(scroll_fade_edge)--
+
+The edge utilities are scroll-aware. Start edges fade in after you scroll away from the start, and end edges fade out when you reach the end. Use `scroll-fade-t`, `scroll-fade-b`, `scroll-fade-l`, and `scroll-fade-r` for physical edges. Use `scroll-fade-s` and `scroll-fade-e` for logical inline edges.
+
+# Fade Size
+
+The fade depth defaults to 12% of the container, capped at 40px so tall scrollers stay subtle. Use `scroll-fade-` to set a fixed size on the spacing scale instead, the same way `scroll-mt-` works.
+
+--DEMO(scroll_fade_size)--
+
+For one-off values, use an arbitrary length or percentage:
+
+```reflex
+rx.el.div(..., class_name="scroll-fade overflow-y-auto scroll-fade-[15%]")
+```
+
+To fade opposite edges by different amounts, use the per-edge modifiers `scroll-fade-t-`, `scroll-fade-b-`, `scroll-fade-s-`, and `scroll-fade-e-`. They override scroll-fade- on the edge they target and accept arbitrary values too.
+
+```reflex
+rx.el.div(..., class_name="scroll-fade overflow-y-auto scroll-fade-b-8 scroll-fade-t-2")
+```
+
+# Disabling the Fade
+
+Use `scroll-fade-none` to remove the fade. It works in any class order, so the typical use is responsive or stateful.
+
+--DEMO(scroll_fade_none)--
+
+
+# Fallback
+
+The scroll-aware behavior is implemented with [CSS scroll-driven animations](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Scroll-driven_animations), with no JavaScript and no scroll listeners. In browsers that do not support scroll-driven animations, `scroll-fade` falls back to a static fade on both edges, and edge utilities fall back to a static fade on the selected edge.
+
+Since the mask is applied to the scroll container itself, a visible scrollbar fades with the content at the edges. Pair `scroll-fade` with `no-scrollbar`, which ships in the same package, if you want to hide the scrollbar entirely.
diff --git a/docs/utilities/shimmer.md b/docs/utilities/shimmer.md
new file mode 100644
index 0000000..89e7503
--- /dev/null
+++ b/docs/utilities/shimmer.md
@@ -0,0 +1,211 @@
+---
+title: "Shimmer"
+description: "Utilities for adding a shimmer effect to text elements."
+order: 0
+---
+
+# Shimmer
+
+Utilities for adding a shimmer effect to text elements.
+
+>The **shimmer** utility is purely composed of CSS and is based on [shadcn/shimmer](https://ui.shadcn.com/docs/utils/shimmer). No extensions to **rxconfig.py** are needed as it uses **Tailwind v4** syntax.
+
+
+# Installation
+
+If your project was set up with `buridan init`, you already have shimmer. It ships with the `buridan` package, which the CLI imports in your global CSS file.
+
+Otherwise install the `buirdan` package:
+
+```uv
+uv run buridan init
+```
+
+You can also copy paste the `shimmer` source directly into your `globals.css` file. Make sure you also include the correct imports, such as `@tailwind utilities;` at the top of your CSS file.
+
+```css
+@property --shimmer-angle {
+ syntax: "";
+ inherits: true;
+ initial-value: 20deg;
+}
+@property --shimmer-image {
+ syntax: "*";
+ inherits: false;
+}
+@property --shimmer-text-fill {
+ syntax: "*";
+ inherits: false;
+}
+
+@theme inline {
+ @keyframes tw-shimmer {
+ from {
+ background-position: 100% 0;
+ }
+ to {
+ background-position: 0 0;
+ }
+ }
+}
+
+@utility shimmer {
+ --_spread: var(--shimmer-spread, calc(3ch + 40px));
+ --_base: currentColor;
+ --_highlight: var(
+ --shimmer-color,
+ oklch(from currentColor l c h / calc(alpha* 0.2))
+ );
+
+ background-image: var(
+ --shimmer-image,
+ linear-gradient(
+ calc(90deg + var(--shimmer-angle)),
+ var(--_base) calc(50% - var(--_spread)),
+ color-mix(in oklch, var(--_highlight), var(--_base) 50%)
+ calc(50% - var(--_spread) * 0.5),
+ var(--_highlight) 50%,
+ color-mix(in oklch, var(--_highlight), var(--_base) 50%)
+ calc(50% + var(--_spread) * 0.5),
+ var(--_base) calc(50% + var(--_spread))
+ )
+ );
+ background-repeat: no-repeat;
+ background-size: calc(200% + var(--_spread) * 2) 100%;
+ background-position: 0 0;
+ background-clip: text;
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: var(--shimmer-text-fill, transparent);
+ animation: tw-shimmer var(--shimmer-duration, 2s) linear infinite;
+
+ @variant dark {
+ --_highlight: var(
+ --shimmer-color,
+ oklch(
+ from currentColor max(0.8, calc(l + 0.4)) c h /
+ calc(alpha + 0.4)
+ )
+ );
+ }
+
+ &:where([dir="rtl"], [dir="rtl"] *) {
+ animation-direction: reverse;
+ }
+}
+
+@utility shimmer-once {
+ animation-iteration-count: 1;
+}
+
+@utility shimmer-reverse {
+ animation-direction: reverse;
+}
+
+@utility shimmer-none {
+ --shimmer-image: none;
+ --shimmer-text-fill: currentColor;
+}
+
+@utility shimmer-color-* {
+ --shimmer-color: --value(--color, [color]);
+ --shimmer-color: color-mix(
+ in oklch,
+ --value(--color, [color]) calc(--modifier(integer) * 1%),
+ transparent
+ );
+}
+
+@utility shimmer-duration-* {
+ --shimmer-duration: calc(--value(integer) * 1ms);
+}
+
+@utility shimmer-spread-* {
+ --shimmer-spread: calc(var(--spacing) * --value(integer));
+ --shimmer-spread: --value([length], [percentage]);
+}
+
+@utility shimmer-angle-* {
+ --shimmer-angle: calc(--value(integer) * 1deg);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .shimmer {
+ animation: none;
+ background-image: none;
+ -webkit-text-fill-color: currentColor;
+ }
+}
+```
+
+
+# Usage
+
+| Class | Styles |
+| ----------------------------- | ---------------------------------------------------------------------------------------------------- |
+| `shimmer` | `background-clip: text;`
`animation: tw-shimmer var(--shimmer-duration, 2s) linear infinite;` |
+| `shimmer-once` | `animation-iteration-count: 1;` |
+| `shimmer-reverse` | `animation-direction: reverse;` |
+| `shimmer-none` | `--shimmer-image: none;`
`--shimmer-text-fill: currentColor;` |
+| `shimmer-color-` | `--shimmer-color: ;` |
+| `shimmer-color-[]` | `--shimmer-color: ;` |
+| `shimmer-color-/` | `--shimmer-color: color-mix(in oklch, , transparent);` |
+| `shimmer-duration-` | `--shimmer-duration: calc( * 1ms);` |
+| `shimmer-spread-` | `--shimmer-spread: calc(var(--spacing) * );` |
+| `shimmer-spread-[]` | `--shimmer-spread: ;` |
+| `shimmer-angle-` | `--shimmer-angle: calc( * 1deg);` |
+
+Add shimmer to a text element.
+
+```reflex
+rx.el.p("Generating response", class_name="shimmer text-muted-foreground w-fit")
+```
+
+The effect is pure CSS. The text is painted with `background-clip: text`, and the highlight sweeps across it in a seamless loop.
+
+>**Note**: If the **shimmer** utility isn't working properly, try adding **w-fit** to the text styling, as it ensures the animation doesn't exceed the inherit width of the text.
+
+# Color
+
+Use `shimmer-color-` to set the highlight color explicitly. It accepts theme colors with an optional opacity modifier, or any arbitrary color value.
+
+--DEMO(shimmer_color)--
+
+# Duration
+
+Use `shimmer-duration-` to set the duration of one sweep in milliseconds. The default is `2000`, i.e. `2s`.
+
+--DEMO(shimmer_duration)--
+
+# Spread
+
+Use `shimmer-spread-` to set the width of the highlight band using the spacing scale. The default is `calc(3ch + 40px)`: a fixed base plus a `3ch` term that scales with the font size.
+
+--DEMO(shimmer_spread)--
+
+# Angle
+
+Use `shimmer-angle-` to set the tilt of the highlight band in degrees. The default is `20`.
+
+--DEMO(shimmer_angle)--
+
+# Reverse
+
+Use `shimmer-reverse` to sweep the highlight in the opposite direction.
+
+--DEMO(shimmer_reverse)--
+
+# Play Once
+
+Use `shimmer-once` to play a single sweep instead of looping, useful as a reveal when streaming completes. Pair it with `shimmer-duration-` to control how long the sweep takes.
+
+--DEMO(shimmer_once)--
+
+```reflex
+rx.el.p("Response Generated", class_name="shimmer shimmer-duration-1100 shimmer-once")
+```
+
+# Disabling the Shimmer
+
+Use `shimmer-none` to turn the effect off and render the text normally. It works in any class order, so the typical use is responsive or stateful.
+
+--DEMO(shimmer_none)--
diff --git a/scripts/generate_search_index.py b/scripts/generate_search_index.py
index 8c2a4d3..24f859b 100644
--- a/scripts/generate_search_index.py
+++ b/scripts/generate_search_index.py
@@ -1,38 +1,3 @@
-# import json
-# from pathlib import Path
-
-# from app.utils.routes import ALL_ROUTES
-
-
-# def generate_search_index(routes: dict[str, list[dict]]) -> None:
-# data = []
-
-# for section, pages in routes.items():
-# for page in pages:
-# data.append(
-# {
-# "section": section.replace("_", " ").title(),
-# "title": page["title"],
-# "description": page.get("description", ""),
-# "url": page["url"],
-# }
-# )
-
-# output_dir = Path("assets/fuse")
-# output_dir.mkdir(parents=True, exist_ok=True)
-
-# output_file = output_dir / "searchList.json"
-
-# with output_file.open("w", encoding="utf-8") as f:
-# json.dump(data, f, indent=2, ensure_ascii=False)
-
-# print(f"Generated {output_file}")
-
-
-# if __name__ == "__main__":
-# generate_search_index(ALL_ROUTES)
-
-
"""
Generate assets/fuse/searchList.json combining:
- buridan/ui routes (from ALL_ROUTES)