From 34dee9d63cdb098e65c6dffa6d32730bdc75051c Mon Sep 17 00:00:00 2001 From: Ahmad Hakim Date: Sat, 27 Jun 2026 12:15:11 +0300 Subject: [PATCH] new: utilities --- app/templates/docsidebar.py | 1 + app/utils/routes.py | 3 +- app/www/library/utilities/scroll_fade_demo.py | 20 + .../library/utilities/scroll_fade_disabled.py | 48 ++ .../library/utilities/scroll_fade_edges.py | 109 ++++ .../utilities/scroll_fade_horizontal.py | 35 ++ .../library/utilities/scroll_fade_no_fade.py | 20 + app/www/library/utilities/scroll_fade_size.py | 48 ++ app/www/library/utilities/shimmer_angle.py | 17 + app/www/library/utilities/shimmer_color.py | 13 + app/www/library/utilities/shimmer_duration.py | 19 + app/www/library/utilities/shimmer_none.py | 9 + .../library/utilities/shimmer_play_once.py | 28 + app/www/library/utilities/shimmer_reverse.py | 12 + app/www/library/utilities/shimmer_spread.py | 17 + assets/docs/utilities/scroll-fade.md | 295 +++++++++ assets/docs/utilities/shimmer.md | 317 ++++++++++ assets/fuse/searchList.json | 10 + assets/globals.css | 586 +++++++++++++++++- assets/social/scroll-fade.webp | Bin 0 -> 18002 bytes assets/social/shimmer.webp | Bin 0 -> 14730 bytes docs/utilities/scroll_fade.md | 106 ++++ docs/utilities/shimmer.md | 211 +++++++ scripts/generate_search_index.py | 35 -- 24 files changed, 1915 insertions(+), 44 deletions(-) create mode 100644 app/www/library/utilities/scroll_fade_demo.py create mode 100644 app/www/library/utilities/scroll_fade_disabled.py create mode 100644 app/www/library/utilities/scroll_fade_edges.py create mode 100644 app/www/library/utilities/scroll_fade_horizontal.py create mode 100644 app/www/library/utilities/scroll_fade_no_fade.py create mode 100644 app/www/library/utilities/scroll_fade_size.py create mode 100644 app/www/library/utilities/shimmer_angle.py create mode 100644 app/www/library/utilities/shimmer_color.py create mode 100644 app/www/library/utilities/shimmer_duration.py create mode 100644 app/www/library/utilities/shimmer_none.py create mode 100644 app/www/library/utilities/shimmer_play_once.py create mode 100644 app/www/library/utilities/shimmer_reverse.py create mode 100644 app/www/library/utilities/shimmer_spread.py create mode 100644 assets/docs/utilities/scroll-fade.md create mode 100644 assets/docs/utilities/shimmer.md create mode 100644 assets/social/scroll-fade.webp create mode 100644 assets/social/shimmer.webp create mode 100644 docs/utilities/scroll_fade.md create mode 100644 docs/utilities/shimmer.md 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 0000000000000000000000000000000000000000..5c6279c2bc16570f5cba72b03de348b492926e4f GIT binary patch literal 18002 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 DPG;8h literal 0 HcmV?d00001 diff --git a/assets/social/shimmer.webp b/assets/social/shimmer.webp new file mode 100644 index 0000000000000000000000000000000000000000..f1115a5cdf4f270dd2fb4dc8cfb197c0b9faaad5 GIT binary patch literal 14730 zcmb`ubC56Hk~Z45ZQHhcw{6?TZrk3wZDY4>+qP}n`rY@O^PQP_=bMSR5%<<#6}497 zTABIe^JG;;DNBiq2XO-dX^4p^sw;94x&3wSC;*%dOj8Ty1|onB7a~FohQkB~WJFKD z6XPvt^FENUn+sU@ZkwEC?B#KJYb5n^??(M5eRcQ{d_ZiA%>~$fnS4)tpL`m$_C)#Q z-tP?dTpHZ_&wg3~(2wOwdtlz39(K+ERXr>Im;ME6~zo9pWZ_q=` zTfjcyw12f<4ZsEv`dty{ZUzwjnz-d+X^xMtJ_8JN`EIPm|cFTg4cJNuraq6`RBw8`ldk=Pyud z&mWEb?OpxozTH>+Yy?-#v)xxr)isWx1`Zvv|9#@)tRpf7pU86Q!%TW5eR@2o@AcnK{nMe*od4aa5s{C^0TS>3h{AtZ{;+Z5Fp>9dqQ8P3+jd@FH$L>} z1Z|uK8p*#*BHr*r0IF9ZAMI{th{Aux{w2<~Wx11I1BH?KaKucC^{9b=!rfse&&b4s zn*N9JZ_B{tk??U-y8jC~8072cFBpzp%*>@&_)#R-66-;!E7r4CSL`wZ9Wif)qdoyW zGaBrvaZe$_8hS|=L~jR$L)9@;1~IlCr6B*n-QOavIrSI%(jVuw5+s#hK=fGKE7}NuxBg#Y z|Iefk-2d+tQ&JqfRm?qo23&CD-QXb`X7U>g=B-HkXa=QRlz!K)ufJ#TQ7bziPhf+a z4-VGE=MU5<9;~D!on^S$WXu7RQ%4VJJSYrVKCIAIhFE_F@^@}k(@Vcm+9`&)*ePUc zdK=fnqK8fx^kWs*%25k-m89ZkSpH?k)(7j|PF9bvRXe6zdGR-m^|n&f1&q)1R4m`8 zLxQkUoq6QR*>Ph&UgE@RA!RC;trsNNKpowflSt4rpRRZ}ogsK)8)+#7UnKhe)%L6b zXFxJbZ>;5|HS2ae-YU= z=Sgv;aAGwqHSM9Dm@il=y;qR14VX!^2Zpv;CC@NyNK_oYqyrlhj)7kr4dPB5)10wrXK=Py`}i zVvEn2SZ8S%{q!RxM4Yf?>mTX-??U;n@BoByG9QrO^b3a>Tl|lKYyVQlJ=W~2e`)D| zkKhJXm?FEwJm zCg)!U*uu>q|0iVqvrAYvisxT9GJ5;L_iy$48@2v}{1*O7%s&L}?~?JK9IPM**&+K6 zP5rm}{r8FgFPa2NAfRu+mp^;UQy78}5YVAGy^csPRiGuvEN1pD%^DV%>u z;BY?>tgx6LcDiAio_z;BxjU>PoMso;YYPzYJ^FSc-A$;QxBDj|gXEH9rhB2!OD5fG zFIL@lfc4}pi~%1=?n8j^pdq493~(t|fT_8gF)#X#-XGW}olsnk(KhF@wt#B&}qO$nqe2)AF zC$4wu7;iIKInw>y(P(df1Z>{V61L~icUie_Gu-O9`|=k|YT_F-YBP^`z-yU(*%g9N zV$@E>@Wn>=l_gywRzj0ufbgF5Tt$U?X7X*mDEzoSN9DGWe^g|)pKHcmyq@|`U-4yg zd0x#Tgs??`mnqbSt}qV^ue7VmA1o*Z=wt=`n>T3Yu2Me#{#=92fsD(!UsYP6yT?^$ z7V7bVte>?TmW2ZkGKOC+QyL=)PfG|~mzLqV2#Et(IPvf)2k$6?p~-EWp1-~!JMRS> zNlp>^yR}+M_3W7iS1hW+ml`@;nCvznEnzr?HJ!hHm_QPr4cj;ZK)1x4&sZUFdhVtG zFH=72x}ney(N|mesB}IBly;YyUu+Luf#SdISx!bUE_S5e;;&kz#3n}}*@Gy(TVkw` zap5gbv{Y*9H$FmlR2Yg#{kqOk?jor}%M@gG5(G<$!OLp>^1Mfsv+#^A9xycp83qo+ zpYreWh3oF4#v9jZ!&fm+3cgq0Vy5t{OWL6=-n2KVEhLvv{G))Lm%Ueznzd6SNU$&2 zJ^SVwoX)`nZ;3NG2P{xCN`3V9r=F6r-d(`8LC8M)mz!LtXX;^p%Y60Ul%*;o_vk-C zdkiM^FP8kd86#Xo3R+whROCM>lSg2BdW)_kOf|X}eV=`u&0BpFGt;(1AGm~lnDe*w z88-jTuCfKz@L6S#)=*>}DL4e=ZNq;Np+he-hz$3P)46-P*7^iO_=+@@xz`Xmr7FO1 zXZ}7Ocs~RwWg78cK(m^U{s9Q@%ZW2vjpIlW^BOdH z+oNK zGSmnis{)SjGXaICGdNwFeEUcn3aGi($SY7u9`GEFVMn_KNL8#H{`*aMkI0oNqKG%s6CbxZF=a9n`xI@`GTfCLW0S zhHbMFGwYrIoN(m)`GLdg|8*|duRaPVnBFxqDjn5wpWDD@mFZoZ{|BEu0##R;f}iVc zJZw`@s(4z$&7UHE>|B&*7oGFO`M0ffhtgQOmTl$Z5>HEl&PWviphoda*8Sv%LC*C%afkzEGzM>%*QEyQR8m+n&2Xt$;mO+-pX{1P)&R@TnI@BCmA*MO!G7_ zyaFCN9Sv0qtv3O?H<~G9k0kPBSIH#5a!%0htet1*nV3T%Zu3avy1O|QK}CA}D=Yv=;s zOscx0K(vys=@dD@-0Kl{z>6Q;r=m9@fuKJpNX3BDcSr#oNqM8Td>pexmuiDwM7|k8 z|Hp~`fj8T2>DH>Ga!MFTE0Hn<3JDayf3)^Q%}LXnUvX~!gl{S)akN~K+nXcpb$Gd| ztR3DHFv@01t{bKB7l*WNXXP$f2_D_jxYs9&5J{9C(QiQo%FiD@A?jznc%^Ci9|RZ3 zevR7f(K5@^pNl{|E=y;OHW2~gvJ8M{IDs|d<~T3o1gMNL&;otfxVHR8<=+hgq8tW(#tTIHXm=~aA1$>4|D3)yVYBKND8+Y1>7G5!_XB>hFn*A=+(rtgs0AX^L5%<3B$RClBX`xLY4JlLTvU5*%j79(HlGv zBBvmHw`=8lK}4UYHp{34O)+bfaV7QV+6#8C&aql*=nwbuvT7c3TKu29oHuhl`;Ch& z87*61`ehlGu${l9&$!`}D3sp>Cs%|t#di%O%ZgPK-Z<3Okt92m(IY_q?Q zpATbMbF(1c$H}6hY<*SwF4#QMrjJu}au;ql#(&D^5a=Wt-ZiW4rMd|}=gq5)2 zJJNa%R<{bu55Sgmzm*_`eyDdXDRq)HJ}c9h*D~ClQ;f zKk@*dMiO}p;D2MY(^OL#5gCO1#gt_~{<2?1^}HDqoUCM|%C=?Z=F(Y;)9lkh{yv8+ zl(7><_)e`wG%TlmG=FAh7?U3v#x1d=iqYkV($~+cPH~$ms$~}5)(ai?rbTc)L58AM zB?IECOOr^WBV6tcNVh}`w-t>zI=P__5Tx80lS}B`+Hd6i@G)yE;#rhZhU&3mBE`sh zMP(d}+WNIa)J`1il1`~%h_26c^v5=N=5AUXM;(|LnVT7Nlj$KhojxyY`-84>>ly=9 z)rdc7auLHC%9h!&+FNa~b6_ZQ%+$nae`{v<`!TBZ)fpkq$t+t=+@7o47TB2-jhVJO z{H6l3#E$RM4+c2S5D4rvX%nEqFi&j{B2WJDUTf)CM=<&zE8F@ut+1T)@ZcWeH+<%j zEcwM#X4r_Qk6|z77{e=(@LL02qX&H9Nzho0tlMVxfEk@+vL1d4wqR>_MqCrW7lcs> zC|&{yA+tuHYq&ng?D=P?farke<*L+mp}?{M!A)s+qT*G53zkW zr9iF=$(9bW-bZV}!L1GYSiiKNq%GouAndAefw75%>nm((&6vWTcEi_#{^To25iE)8 z%hJG`%MS6}%0WdYmU7+7hYIx_<`h$Vs#pf;T&#IN~BJGzwDoaSveB86CfSqvPj3%1y@{3(v_o|FYGryecR zs?rd_>jGuR3{RN3V;y@f^`8Y6{e4c2lrr85PVVRHNJ!Wk(xyE@>Ni@M4#pgqDrUYW z;J@w>9)?fQ=}D^LOBI&}176{9?{x$?=y>I`Aa;60H(>zA&#+){Uct=qvc97a$8=PD zpz2gofET{n+CBR0lgdK;r7F#4n9^m7@m4Ar%`0+T8gYU4s8CE+u%V^Wmao>9^QoA* zH{vL!J5<3Lzgs>3V-;rn%T$uKuILx8&phQ7a>U$_OvuVuJLMi@oEK6S8$VaUPa)>j zd-Om9f7N@B3Xors{qTE?A<32D&5P2O8ERKYm)r1iy%vuqi6U>VDzN0}kf*&6V1$A@UAPs> zAAd6ErsL4D7~B-HK1Onh*D)3)@b()T7gjHTY8=XM`C`y!mXg#Ds#q9Uh3=G&Lo`AZ zP4pY;=zW>`glSIW6_CHU36L&B2l4eup+W`%t*PCMtPNtp!eo|rxnNI(%Ui@kGIT-V zh6B9qY}u)u9yRsXkl^t&VT$OG?lcN2JSrb1-P@{EUh-BGm1GBO4cjtQrBNN9BozCN z+a-Ls431GAe!SfbToei`yIF-+^|~0(srDZWQM|goeJmpY6F-X&P4rsp-w$loR!+GR zVj3wmj2^;i%AT$hGqcjyKzNOYmn{i~ZY1?e*RH3p$9l0*MduEgTX;w|K8vK2bZHyC z8LIk4Fzz6ixx~wP`WtZ9*NMSIeR>gX(JY%5K{0aoD+y?zWkEz4+-^lzgjya zvX$8o)-+~zVwt(@-m*JxMiqI~r2l0ERW`?jkT^J@o~g8UM~z)kLlJa;J|ASv2wtXH z)4%fBc*@FcBSd@Ix6Or9p3G&z+28q@C^;|7UeB@m27!f+rJlPw8(2o6Py?*>Gc>RH z!6*?^Vo-qUg*O47%eZWcDE1B{XF7Ft#G+gP((c!h;Y?D8Lc2o%4H%IGll8WZm-1M@ zamORVxcIp$UX)T%aInLg2;0-+o6jIC1>2Q9MrTDf{4{8KcSq^sK{KUe z@#2Fpxx>qO=ujp4vQ20no)!0Sy+-vTwi3Sg*eI`PgFxHsDsHo)(yQoIsAb%;1pxr7 zG-{2I1MJqVia>tGr3x2CcfP@^V3uNu)e-Qk?hm18;}QkgJ&5Wi zsfYW7b@3%lDh}3CwFw=62EI|nr}&9`JZ(WAJm_F?@%-2^=o-jAYGsk`GlSe?>hAk# zP~<|Aw-^X-GnGEi`E%vHlt8R`resi!)pCddAsiSR7hy%hZ1sRPdoiW>7FU@ByVnvf zso_AM{`gjq$!F`$W-D|~!&zWn3@^mdTy1gpo1D@aSx<&NM@UDW#urM|^-U>Y2M)rgb)5qm8(}7*#fnv_bP#JUVUi#%*ki6r zb|_>TONlO8wJqzwU-J33%N%~e*G|OnGT}!cEi}vg^94$7jG#kzy|~9#ag2MC=UnT% zNN2lD$rh;jlKxqygD}h%4ekdX2Q>~<`%2mUZ|Ln8*5=yyT?biB+t>E_m+CJDaA1L} zt6X!a5DV2ex04B5vzL4wJ_e*p@I!-}OFwo#O^IXRbzGy-yuqDpu8h{^;U_}Nju-=tZdU$5GCZE~2Dcw(5J zknFIF%_Vz{>7SJlqVXx2k7DZYuDpM0|%1wNd zWfBOMeI68w{E51z0!KvH21v-rwuO;al#+nXrLxoPK+eR^^^Ao-0$EGMH3n~}M}@(b zJT=8jZ|Ls_c?+zPBk1aXvWnYsyEqZ}bGxpiCiEI)G=hSdeh98>77}{Z*v@{Gw%lhPSu}r#^sFI0~>UQOyNuKUMh)%DeCswC^U1J5fdb9MexVFOraV zvpCll?aVGfJO+_fnA>nG57^zw>Fgy9aQ~T4H41(BX)WBGC$esYg;#8ST=EUIdW3Mt ztn=K<6rlAv!xh%|5NREfnejp`x6NKiW8fJyX?uQuSWW2VBAcIIn! zWFN^eUeUF!sZjd7w`t!~?ZgnGWqz&1=`)nniMFsi<&EC_E>#b-|LJ%YxJloJfPdGR=#gMV}5Q~7k zJ;3^6mXFL7ZUmKB?O#q235Hase!2maaNNgN(b_m&fvBWEXy_Ih7h?MialMKwx&j^J zt5|tDiU^a zwmiyE`)~-a1*p3%eEvnqut33LNxu+C$^WW_Y9o(bV3>J6ITUExRLfkp;j zS)*;sF^58jp@xtB&XeNBm<0+X3i)q|pysLEunYpJth02NzsUwNO<~m2>}e|}y^$w? z%F$U_)52Iy7loh{9s9mP^(7ypq9>sa4~^i<=7D}wkBRK3Kj(LmEe@*K9dje0p*Pzf zNGV|#5fXMo;|(W%J%2Wb`*+x)i8$Cz^2Jr05IZej-aa3l zk!?kSXz_h{bRFSwC!U$ZDlr)2!6qIl-_QL_po;*Wa%#l#kQ_~KOvCeE@FN1#Ur3Ol zv#{Z}`(|Q{jnI2d&G=T$-s!5717Qz6bw4vn8Vb178F1x)fbx%zTa-Xo;z?5i6RO(? zK-Yx4)N^0j-{&uVLP3&_4BT~kS`pA5C9l9&x`1*!S5u+^|C|b-zw5P#>uh=>R+{#g z3o0>)q_Jg74~DuAFa)(2FiJg9erMT;iq#BrePa>{s%XT=^1LmY8W@T>O5707rNl(9 zsL|>W(j3%~X!}_Ifh|4;ZKS!{6k5eV_THZ76zvXjiG|&@I(=BmBO4BdB6(G`6Z7JH z)rKJ_J2GErpYBke{`p6*!EG62aCp3(O+>-8x}gpfx?*k;uVxiVC`br8Y*w`Ybq|!% zbi3F>F#kR))i{>}$X~zl`Z5ePm;sHsQDmUu%Gr8vIjxP9ZSE_Y7bB1J83TEORWT*1 z#)Gaw_{{GI=^2?04KT5lw>0*^X3nG-e9^cFzdh?OtWvXk>C*G}ste%>3MWPv4of1_G$P=T=W z!F;z(+)J|R3yakboGgqX{Wg9LJly35C@M1)B4%iCQQoy(i6>-mGLqytrk#jL%R9T_ z@t&mzFbJ4;0zLt9R7he)!K zeaIVhDsuKJCJ&uk+kVhj>jzw+3@(x-XPYCwdwSA+GoJN`=;!(mUooZP8dZ!_Zo`n z293V1Yc2a|pZ_p`iFsh19#n*if|i$6@%n>%;MfLQb_gp6O(EhaQh`62OI&hX0=Jg; zxYyIuapKUFWZB6Bd z&}8SC`0Rxs;rWfk-*o74iDKlFjjkY5A{_3WdHfuaR{Yv8e@&9zx5%03{7ichft=TobFeMj4;0SuHNJ45 zjI3?!u!t(o($=}RKj)3`^C^WPkl;T732H+bJ&cFAo?{J^3kK^O3eVWfAE&s6?>TGt z$SPNpiJOO>7h`@m@dV6B#lWi*4^mPX^C?R}|4Gv_bt@`xS?Ay>>Z3MwB4FWTjNtI^ zVN8ycwA$d56xa_Xp@3t4Jn|M;b**l?$YW4p2MYAM+W8F=%@#M;M{b&*<>1xa2n&b# z3n=?LgTcCg9<6eYy=DgE?)}`wJ0mz}J#wdyKj0ZpC>d}Ydr5kdqnoVl z78%U)ZstiJGUF(raAzO4S_Tw-8FiVU69NNqNGHzT#L(sxqLx`|yRnm8=Ph-RO4m6n zjP%~SP(cXv!!uUDP=aGyMBEUlvu5nc`&xqF2=L*?3d7iIJbsZjhG~?y$9pUG}ut5Zc zb>U6CFQ9sQ@I4hI0TH!X-Z{Bb`@2y1g329xc-XvHwZoP~#fxCtU|N!_HdTliNR@7_ zVh6~t|7_GI22K>cYt>geR~jkkZ8x&%*P_!;dids{%C7=75>^{1h|4MKo9jq3DLCXX z+@0ZLW=Fa1I5z~xA6rwYhCzeTxGIxVjh{S7SiFH=ndtGvuow$@bseljWa{k*P0g^` zLd(f1y}0&~shA&Maxv`YHvFQnfbuBHfY&PSS!@&1+IY+~EvPB8v-&^Zk_4!nuUDU0FD{ zoXX98`9=NNIWYWHjr$u(>Y>l<*u&!x!z80?BB0S4~M>UOMw#0z*4|Wxa z7_-Rv@u=iSSwH}dMQ0RnIP~rf?wE?S0y(OqrKCAMKQrBCFQ+HUL`Jk z`Vx$}TH=sewn)fUotPYXwYc?xK8$0}N+5f^UEi3t`ikw`x@x6LlR2U4M}9miBrAPl z5Swb&24)QMB7)IvP}QWRp^&1Qw#06Zb&mgN=qX=~cUsE?QG@ojhVq zK4`nPUSeesQowZ@6E%Iv%WU4~oS&^4g@4sW#Mkkzu{gMvzmdU@rNmZYrh)BlkldR? zlP4kK5EmMomaP@9XAWzA+oo>PCI8yaM88w8+FS}h28-WQ#77Y_c)mlpa#8`w!Vy&- z?IT^|yeuCSq60Zs98YC1Qt1!-n$5p;ceYEwxF`r0r&1P`&_n84ctK9e!7E~BkyAEQ zt7=z(T=BhTMWcDv_{?9y_RG3>3ZhFLiw(M!#2+wGlb*J^4Vt*;ZRuyEbj;=7N{qjB z**QN+u(XVrb2^o2aC+m|9jY?|CesCibFhF^b@DvOZ!8SX)Z37^Yiz3>Zs-YL7&hWf z7bksbIL=bu%W}unu{p?&0d+Z@Iis^vl5W`{(~)a#=-9KRsKE|zl0twOuqqa^6D@kh z9cdZcE!#Mu`|v?|S5e7Eln#--pGR-{ZM|{&{5zFVW-M;pKJ*RYqM7HvP;mC*=FY-h zu5iCgOrbj&Stb-s9lb@PVeG$`C&WL9aI+1(XE{4~0cr<{EVeJDwnFXKY{HQp(bU|l zG5sJe;(72u^cbsnhR<0FmUHs=`6^r^5Gexui>+R(e~73et>`;D|I>$&PlO=)(ZPpl zCHjWE2TPw$W4a8G2|a`fsi43(r6PJ(SzTQJ1($nH)G+lybA8LJ3b}Sdj_h3SSNy*k z!V+lASkgGg621gopf}hu8PK>eEtWvSuhx{{puMh)r5R-;w_0toWs34TALqzma`2JP zL_|r0$W(h!(o+8wG9F)k#+E>m4%O;Yri<^qUNMR?G{m0`EImW%HTj1TI?a6I-0fMw zNF(X#u8=~vQ;Eo?JM_ICPAv(#v_Tc99*XSS=?oI{@iRFPD^avmQ#NxZ(VdrzR z8wL(cN$>gv9!6js#Ok-ru3*%CBGaAStY6xoc(#R6S(EQGQ9E?Hk5m9 zeqIvUfr`FFvVAIHL2lU>we97xacfOYkUIpW9h$JA>szt^N+B=eKEFr~!13)16^_iu zOnfzbwVCBW0FoBGo{nk}{Ue{k9s6ZC=f@j?StSf8;lWpuD$P~kYR*feroYa*ySK$z zCM1Baj7E@Ntw=-^o%a103kGvg3$}xEbE_&n5D|ygRwDPE(n1^K?dWA?om=LO-cvg0 zkZ(o>#;Z$t+^Z_AJ@AH7UN=-v+C=8n%|tgl;9Oa~7`9ld4!E9dEIAj>OT;IQ4t|pJ z2pCzEXGK5XZgFoRXldK(+Cl%_m2rm_;I>NPhx>}kDs9zNNgpniv%PrqOxuIsXaxJs zqvq($r5|7E6!UozeBo!fVrT+g#|I)*Unalavz^bLK}#9@i)0kv{&zRP(HSs+ceI~_ z?VXObw{*-keZmFAhXJh6#=c-UtP`n7)A$mmwhVFG8|2bnja)^YV|}NLEpg3*Z6^|| zrTlxN`ruEZ*1cs1K~uCwrD|Vo-0aJ)V|ubu zG41-<(v~emVA%P^Lg^;Bz(97X-btJ;Dx|UaXwo4}qWwfzPJOCKo_Xi|5Gc7Z0+3+F z^6}$by(0caJ1%~K%!%cZp>pUBP0{jz$fAWIb0oCsMk58yK%9QA7_LzY2!BsiCiL}C zY{$Y2W*&>bf#ivu7x9#lr8W%(UOQ`${GZ&C%FTc|J^H|n-oQx5VX>^QwkT~FQI8sk zK1uhU2p~A0wb=(hD=cUCVo30%kg;$N5MUitDoD|Q!e`^|%~N4rozFMVWgGh#2xoVF_N3P*je+TWH`JZyiV1O{ITncXz2`ch$4LbR7uG`vmdAA zYRK2NghF*r$|!7uCUH=2ZW+e2%+g$z*|I-`dlnaGuEgmQ#|j@|$pJ$U$xI zq7#h&DFOg{YIxwlAER+hjDH-RO&7C*N=jg(tq6@K9!t+jr8rjfI+rd&SlM@Q(OU^s zcoj=~$)0R-9{Yls{XCk8$>~xKKwm^|xTLG~* zu|mqB&wbr@%6gyPe_DMoi^^)dzbvRKeymDsKu~{3#W;3ckbAA3#R~~*N8xxh6?#Sp zjZJ=pw@kH1LeTTN$F|e~2XW|=^c!-;Qdb-m9C&?@5`vXaeGNW!#geBz>khiml9=gb z+F7g3_4bq`Qu;eaTBPKrI`?Z1l5SVr!g)X`H&Eaq(kEyq?>tDZn@u;2<5O2C@L3M! zuD;nx-V?S}25iAy-p-sJxurPv~h(JBM+ zN_(Oz50ZeMsN*wG(Q$M}28Og%wG!v@YXKGh1Vfae&CGQ% zUY6jg1vxe8RW;zO5`oDn#ReU<6|b%Kx&q~an?%SjttPP+ z)60_XE*^w+apMsvsK)>&R-uNH@HK5O(W9Moci6TNdr3Wg%NKs3>|h{@^RXRX$&U1# zb_=H!rwR_C0Bq{KZj}P~Loa!^}OrRfHS@f?? z+4YFQL##hAkaR5eAvwl|I+Iw4!H2YrbJIU1w+SdH`&_x0HZ6;$!Y_&jP zee{=zkDrg@!t)?NphR2{4Z>BA5(g0&LOplfj2*GyEiwx=;OWWTmMUjWxmNSfT3(J- zwug-i@@)T@hCM1{RZ*_IPG#QyY_YW_;|hLv&_ro(5PHn_GTdvx#)d4s*T0Ql&{}bC z7aJJ}5}Z*-ru0=euKXIGCNg&#{GJQZSo9jHxBSU&_E@q-9NTpqOwb<#Or?g`MTHwd z&Ks=?BI#5drj+X5kTf|Hc2VoUOMg^-D(}Z>f?vKws9J9)4{PZ{8p&xDtt*fEr|qro z>jA*mBNZnH!X|u$$$t0!NLcGH9mJR8k9kb!OWBf2|G^WuC4tCGeMead%9E?i(6%~y z2nr7sP6CCX#z_jNJw>dUb&5*Ml3)Mmqn^=ta*7u&NNu{ zZNx7r-f4nE0*;+f->w4xr_WadGIpSGRE!>{P5cONyqk_Dvxkqx3Xy{Nqt(%86g;aF zlqO629aQmI`WBAM`JqXt2Ti~vw}Rtb(^-bw4BUs{)xfXUGGHiadkXc|l_~GDWI%-q zAd$%7_}4!gkIXu66?%U?b(8B#qAZ2Y5q3gU9fmmRiS1PL{UYt38<=(nnKO(U@0TDd zk68S*v$gw3`^s5ei^&0JFikFKV7?jTL@kxFGLX_@Z{L{Di7_nE`WykMOKP5jex5WM z7Bcbd`=w+iS=YzjO4Nv>B`C|5S?5Y0%|#2c!Le(Akcn#>cPPUh#B}Qz!;gA5{Xs%2 zK)*UL8-^x>=RHB}qXaTZ_o!up*K+#9ib;wB+hBn^db(W!B+%&+K)feNMRPz{isW$; zfjn<*F>7@kG{hUpxdkbdYYRqG67 zqBS-+xidX``yM-#_#ov0N<`PT%F=4NYiQ!&)wOs%XY)@n+D1L9po4XyNFqH6u~LUg zrtM&`z$h2oX*g2-p=%oGWMF}7#&i#MIDu}~W{SbA2r(wC@av#hqt*s#Q2Puo8C|N5 z>cqjbaSJ@y#n!q-T^eR%6RF$$M>QfeO5oRi6Ak%_@8?DHOjv6uZ*cXkoF6_Pv^-t^ zIf4$G$iXsRiHX#n?xnN32uY3}+x^N(yIB9wdIGmuT7i(n(pkc8nQxXBLP(ChCW8p~ zamjesTiZ$5y;cQGJ&UnLWhh1jp2-6Iqe|b}FG>u%hrhn!M|3{)kyfFfxIl5(cFP!a z`;hzn+}Z_xBWq|rvW@nm&;$$hosY+V{SnQXec?oqCuQE}I)fo$3)y}3vc-HuTLyP! z0h+4Ye8`A|BiB{7SE8ddJz*kMQc23TVR7~yCURL2*{{U$iTmW$*q|-;jc(0;wyc0A zd3>0yD{zNOv+A&e75i}TGXL>jN;7IH7>9G&3phgh~1 z0W7;h6nR4bnE3wyZd@R& literal 0 HcmV?d00001 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)