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)