diff --git a/app/examples/components.py b/app/examples/components.py index 0541905..051d7a8 100644 --- a/app/examples/components.py +++ b/app/examples/components.py @@ -36,9 +36,9 @@ def card_one() -> rx.Component: ), rx.el.div( rx.el.div( - button("Default", variant="default", size="sm"), - button("Secondary", variant="secondary", size="sm"), - button("Outline", variant="outline", size="sm"), + button("Default", variant="default"), + button("Secondary", variant="secondary"), + button("Outline", variant="outline"), class_name="w-full grid grid-cols-3 items-center gap-x-2", ), rx.el.div( @@ -53,8 +53,8 @@ def card_one() -> rx.Component: class_name="flex flex-col gap-y-4", ), rx.el.div( - button("Close", variant="default", size="sm"), - button("Send Text", variant="outline", size="sm"), + button("Close", variant="default"), + button("Send Text", variant="outline"), class_name="w-full grid grid-cols-2 items-center gap-x-2", ), class_name="w-full flex flex-col gap-y-card", @@ -65,32 +65,24 @@ def card_one() -> rx.Component: def card_two() -> rx.Component: return rx.el.div( rx.el.div( - rx.el.p("Project Team", class_name="text-lg font-semibold text-foreground"), - rx.el.p( - "Manage collaborators", - class_name="text-sm font-light text-muted-foreground", - ), - class_name="flex flex-col gap-y-1", - ), - rx.el.div( - rx.el.div( - avatar( - src="https://avatars.githubusercontent.com/u/84860195?v=4", - alt="@buridan-ui", - fallback="BUI", - class_name="size-12", - ), - avatar( - src="https://avatars.githubusercontent.com/u/104714959?s=200&v=4", - alt="@reflex", - fallback="RE", - class_name="size-12", + avatar.group( + avatar.root( + avatar.image( + src="https://github.com/reflex-dev.png", + custom_attrs={"alt": "@reflex"}, + ), + avatar.fallback("RD"), + size="xl", ), - class_name=( - "flex -space-x-2 " - "*:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-[var(--background)] " - "*:data-[slot=avatar]:grayscale" + avatar.root( + avatar.image( + src="https://github.com/LineIndent.png", + custom_attrs={"alt": "LineIndent"}, + ), + avatar.fallback("LI"), + size="xl", ), + class_name="grayscale", ), rx.el.p( "No Team Members", class_name="text-md font-medium text-foreground" @@ -132,10 +124,10 @@ def card_three() -> rx.Component: ), rx.el.div( rx.el.div( - button("Cooking", variant="outline", size="sm"), - button("Dining", variant="outline", size="sm"), - button("Light", variant="outline", size="sm"), - button("Focus", variant="outline", size="sm"), + button("Cooking", variant="outline"), + button("Dining", variant="outline"), + button("Light", variant="outline"), + button("Focus", variant="outline"), class_name="w-full grid grid-cols-4 items-center gap-x-2", ), rx.el.div( @@ -160,7 +152,7 @@ def card_three() -> rx.Component: default_value=item["value"], ), ), - class_name="w-full grid grid-cols-2 items-center gap-x-2 border border-input rounded-radius p-2.5", + class_name="w-full grid grid-cols-2 items-center gap-x-2 border border-input rounded-lg p-2.5", ) for item in room_controls ], @@ -354,7 +346,6 @@ def card_five() -> rx.Component: button( hi("Cancel01Icon", class_name="size-4"), variant="ghost", - size="sm", class_name="!px-2 !py-2 self-start", ), class_name="w-full flex flex-row items-start justify-between", @@ -391,7 +382,7 @@ def card_five() -> rx.Component: "Business Account (--3301) — $5,200.00", value="business" ), class_name=( - "w-full rounded-radius border border-input bg-secondary " + "w-full rounded-lg border border-input bg-secondary " "px-3 py-2 text-sm text-foreground appearance-none cursor-pointer " "focus:outline-none focus:ring-1 focus:ring-input" ), @@ -409,7 +400,7 @@ def card_five() -> rx.Component: ), rx.el.option("Roth IRA (··7745) — $18,300.00", value="ira"), class_name=( - "w-full rounded-radius border border-input bg-secondary " + "w-full rounded-lg border border-input bg-secondary " "px-3 py-2 text-sm text-foreground appearance-none cursor-pointer " "focus:outline-none focus:ring-1 focus:ring-input" ), @@ -436,7 +427,7 @@ def card_five() -> rx.Component: ) for row in summary_rows ], - class_name="w-full flex flex-col rounded-radius border border-input px-3", + class_name="w-full flex flex-col rounded-lg border border-input px-3", ), rx.el.div( button("Confirm Transfer", variant="default", class_name="w-full"), @@ -504,7 +495,7 @@ def card_seven() -> rx.Component: "Pending Setup", class_name="text-xs font-medium text-foreground", ), - class_name="flex flex-row items-center gap-x-2 px-3 py-1.5 rounded-radius border border-input bg-secondary w-fit", + class_name="flex flex-row items-center gap-x-2 px-3 py-1.5 rounded-lg border border-input bg-secondary w-fit", ), class_name="w-full flex flex-col gap-y-3", ), @@ -532,7 +523,7 @@ def card_seven() -> rx.Component: ) for row in summary_rows ], - class_name="w-full flex flex-col bg-secondary rounded-radius px-3", + class_name="w-full flex flex-col bg-secondary rounded-lg px-3", ), # Footer note rx.el.div( @@ -640,10 +631,10 @@ def goal_item(goal: dict) -> rx.Component: # Progress bar rx.el.div( rx.el.div( - class_name="h-full bg-primary rounded-radius", + class_name="h-full bg-primary rounded-lg", style={"width": f"{goal['percent']}%"}, ), - class_name="w-full h-1 bg-input rounded-radius overflow-hidden", + class_name="w-full h-1 bg-input rounded-lg overflow-hidden", ), # Percent + amount row rx.el.div( @@ -657,7 +648,7 @@ def goal_item(goal: dict) -> rx.Component: ), class_name="w-full flex flex-row items-center justify-between", ), - class_name="w-full flex flex-col gap-y-3 bg-secondary rounded-radius p-4", + class_name="w-full flex flex-col gap-y-3 bg-secondary rounded-lg p-4", ) return rx.el.div( @@ -674,7 +665,7 @@ def goal_item(goal: dict) -> rx.Component: ), class_name="flex flex-col gap-y-0.5", ), - button("New Goal", variant="outline", size="sm"), + button("New Goal", variant="outline"), class_name="w-full flex flex-row items-start justify-between", ), # Goal items @@ -756,8 +747,8 @@ def card_ten() -> rx.Component: ), # Buttons rx.el.div( - button("Discard", variant="secondary", size="sm"), - button("Save Changes", variant="default", size="sm"), + button("Discard", variant="secondary"), + button("Save Changes", variant="default"), class_name="w-full flex flex-row items-center justify-end gap-x-2", ), class_name="w-full flex flex-col gap-y-card text-foreground", @@ -782,13 +773,11 @@ def codespaces_panel() -> rx.Component: button( hi("Add01Icon", class_name="size-4"), variant="ghost", - size="sm", class_name="!px-2 !py-2", ), button( hi("MoreHorizontalIcon", class_name="size-4"), variant="ghost", - size="sm", class_name="!px-2 !py-2", ), class_name="flex flex-row items-center gap-x-1", @@ -809,7 +798,7 @@ def codespaces_panel() -> rx.Component: "You don't have any codespaces with this repository checked out", class_name="text-xs font-light text-muted-foreground text-center max-w-[200px]", ), - button("Create Codespace", variant="default", size="sm"), + button("Create Codespace", variant="default"), rx.el.p( "Learn more about codespaces", class_name="text-xs text-muted-foreground underline underline-offset-2 cursor-pointer", @@ -854,7 +843,7 @@ def local_panel() -> rx.Component: "Clone using the web URL.", class_name="text-xs font-light text-muted-foreground px-1", ), - class_name="w-full flex flex-col gap-y-2 border border-input rounded-radius p-2", + class_name="w-full flex flex-col gap-y-2 border border-input rounded-lg p-2", ), class_name="w-full pt-3", ), @@ -982,7 +971,7 @@ def card_twelve() -> rx.Component: ), rx.el.div( hi("Cancel01Icon", class_name="size-3.5 text-foreground"), - class_name="p-1 flex items-center justify-center bg-secondary rounded-radius", + class_name="p-1 flex items-center justify-center bg-secondary rounded-lg", ), class_name="w-full flex flex-row items-center justify-between", ), @@ -1005,7 +994,7 @@ def card_twelve() -> rx.Component: rx.el.option("EUR — Euro", value="eur"), rx.el.option("GBP — British Pound", value="gbp"), class_name=( - "w-full rounded-radius border border-input bg-secondary " + "w-full rounded-lg border border-input bg-secondary " "px-3 py-2 text-sm text-foreground appearance-none cursor-pointer " "focus:outline-none focus:ring-1 focus:ring-input" ), @@ -1039,8 +1028,8 @@ def card_twelve() -> rx.Component: ), # Buttons rx.el.div( - button("Reset", variant="outline", size="sm"), - button("Save Preferences", variant="default", size="sm"), + button("Reset", variant="outline"), + button("Save Preferences", variant="default"), class_name="w-full flex flex-row items-center justify-between pt-2", ), class_name="w-full flex flex-col gap-y-card text-foreground", @@ -1144,17 +1133,17 @@ def day_bar(day: dict) -> rx.Component: rx.el.div( # Grey background portion (remaining capacity) rx.el.div( - class_name="w-full bg-secondary rounded-radius", + class_name="w-full bg-secondary rounded-lg", style={"height": f"{100 - day['calories']}%"}, ), # Green filled portion rx.el.div( - class_name="w-full bg-chart-1 rounded-radius", + class_name="w-full bg-chart-1 rounded-lg", style={"height": f"{day['calories']}%"}, ), class_name="w-full flex-1 flex flex-col justify-end gap-y-0.5 overflow-hidden", ), - class_name="flex flex-col items-center gap-y-2 border border-input rounded-radius p-2 flex-1 h-36", + class_name="flex flex-col items-center gap-y-2 border border-input rounded-lg p-2 flex-1 h-36", ) return rx.el.div( @@ -1225,7 +1214,7 @@ def card_fifteen() -> rx.Component: def bar_column(height: int, color: str) -> rx.Component: return rx.el.div( - class_name=f"w-5 rounded-radius {color}", + class_name=f"w-5 rounded-lg {color}", style={"height": f"{height}%"}, ) @@ -1289,8 +1278,8 @@ def sleep_group(group: dict) -> rx.Component: rx.el.div(class_name="w-full h-px bg-input"), # Footer rx.el.div( - button("Good", variant="outline", size="sm", class_name="rounded-full"), - button("Details", variant="outline", size="sm"), + button("Good", variant="outline", class_name="rounded-full"), + button("Details", variant="outline"), class_name="w-full flex flex-row items-center justify-between pt-2", ), class_name="w-full flex flex-col gap-y-card text-foreground", @@ -1417,7 +1406,7 @@ def card_eighteen() -> rx.Component: class_name="text-sm font-semibold text-foreground", ), rx.el.div( - *[button(t, size="sm", variant="secondary") for t in times], + *[button(t, variant="secondary") for t in times], class_name="w-full flex flex-row flex-wrap gap-2", ), class_name="w-full flex flex-col gap-y-3", @@ -1429,7 +1418,7 @@ def card_eighteen() -> rx.Component: "Please arrive 15 minutes early.", class_name="text-sm font-light text-muted-foreground", ), - class_name="w-full flex flex-col gap-y-1 border border-input rounded-radius p-3", + class_name="w-full flex flex-col gap-y-1 border border-input rounded-lg p-3", ), rx.el.div( button("Book Appointment", class_name="w-full"), @@ -1520,7 +1509,7 @@ def swatch(color: dict) -> rx.Component: border_class = "border border-input" if color.get("border") else "" return rx.el.div( rx.el.div( - class_name=f"rounded-radius {color['bg']} {border_class} size-12 flex-shrink-0", + class_name=f"rounded-lg {color['bg']} {border_class} size-12 flex-shrink-0", ), # Updated class_name below rx.el.p( diff --git a/app/examples/utils.py b/app/examples/utils.py index 2a0f20e..918a597 100644 --- a/app/examples/utils.py +++ b/app/examples/utils.py @@ -33,7 +33,7 @@ def open_in_reflex_build( tooltip.positioner( tooltip.popup( rx.el.p(tooltip_content, class_name="!text-xs"), - class_name="rounded-radius p-2", + class_name="rounded-lg p-2", ), side="top", side_offset=8, @@ -134,7 +134,7 @@ def wrapper(*args, **kwargs): "break-inside-avoid", "w-full", "bg-card", - "rounded-radius", + "rounded-lg", "border", "border-input", "p-card", diff --git a/app/templates/options.py b/app/templates/options.py index edabb6c..08989f8 100644 --- a/app/templates/options.py +++ b/app/templates/options.py @@ -231,7 +231,7 @@ def menu_panel(desktop: bool = True) -> rx.Component: class_name="" if desktop else "w-full", ), ), - name="radius_select", + name="menu_panel", default_value="Medium", ) @@ -688,7 +688,7 @@ def open_preset_menu() -> rx.Component: input( placeholder="ex: b2D0wqNxT", id="seed-input-el", - class_name="rounded-radius text-foreground", + class_name="rounded-lg text-foreground", ), rx.el.div( dialog.close( diff --git a/app/templates/toc.py b/app/templates/toc.py index 5eb06be..c98d9c1 100644 --- a/app/templates/toc.py +++ b/app/templates/toc.py @@ -45,7 +45,7 @@ def create_copy_button(url: str) -> rx.Component: tooltip.positioner( tooltip.popup( rx.el.p("Copy Page", class_name="!text-xs"), - class_name="rounded-radius p-2", + class_name="rounded-lg p-2", ), side="bottom", side_offset=8, @@ -114,7 +114,7 @@ def external_tool_item( tooltip.positioner( tooltip.popup( rx.el.p(tooltip_content, class_name="!text-xs"), - class_name="rounded-radius p-2", + class_name="rounded-lg p-2", ), side="bottom", side_offset=8, diff --git a/app/templates/utils.py b/app/templates/utils.py index 0564537..332cefb 100644 --- a/app/templates/utils.py +++ b/app/templates/utils.py @@ -12,7 +12,7 @@ def attach_tooltip(func: rx.Component, label: str): tooltip.popup( tooltip.arrow(), label, - class_name="rounded-radius", + class_name="rounded-lg", ), side="bottom", ), diff --git a/app/www/library/examples/avatar_basic.py b/app/www/library/examples/avatar_basic.py new file mode 100644 index 0000000..4a80d04 --- /dev/null +++ b/app/www/library/examples/avatar_basic.py @@ -0,0 +1,13 @@ +import reflex as rx + +from components.ui.avatar import avatar + + +def avatar_basic() -> rx.Component: + return avatar.root( + avatar.image( + src="https://github.com/LineIndent.png", + custom_attrs={"alt": "@lineindent"}, + ), + avatar.fallback("AH"), + ) diff --git a/app/www/library/examples/avatar_dropdown_menu.py b/app/www/library/examples/avatar_dropdown_menu.py new file mode 100644 index 0000000..962fc87 --- /dev/null +++ b/app/www/library/examples/avatar_dropdown_menu.py @@ -0,0 +1,36 @@ +import reflex as rx + +from components.ui.avatar import avatar +from components.ui.button import button +from components.ui.menu import menu + + +def avatar_dropdown_menu() -> rx.Component: + return menu.root( + menu.trigger( + render_=button( + avatar.root( + avatar.image( + src="https://github.com/LineIndent.png", + custom_attrs={"alt": "lineindent"}, + ), + avatar.fallback("LI"), + ), + variant="ghost", + size="icon", + class_name="rounded-full", + ) + ), + menu.portal( + menu.positioner( + menu.popup( + menu.group( + menu.item("Profile"), + menu.item("Billing"), + menu.item("Settings"), + ), + class_name="w-32", + ), + ) + ), + ) diff --git a/app/www/library/examples/avatar_general.py b/app/www/library/examples/avatar_general.py deleted file mode 100644 index 7e755b8..0000000 --- a/app/www/library/examples/avatar_general.py +++ /dev/null @@ -1,42 +0,0 @@ -import reflex as rx -from components.ui.avatar import avatar - - -def avatar_general(): - return rx.box( - avatar( - src="https://avatars.githubusercontent.com/u/84860195?v=4", - alt="@LineIndent", - fallback="CN", - ), - avatar( - src="https://avatars.githubusercontent.com/u/198465274?s=200&v=4", - alt="@buridan-ui", - fallback="BUI", - class_name="rounded-lg", - ), - rx.box( - avatar( - src="", - alt="@buridan-ui", - fallback="BU", - ), - avatar( - src="https://avatars.githubusercontent.com/u/84860195?v=4", - alt="@buridan-ui", - fallback="BUI", - ), - avatar( - src="https://avatars.githubusercontent.com/u/104714959?s=200&v=4", - alt="@reflex", - fallback="RE", - ), - class_name=( - "flex -space-x-2 " - "*:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-[var(--background)] " - "*:data-[slot=avatar]:grayscale" - ), - ), - class_name="flex flex-row flex-wrap items-center gap-12 p-8", - ) - diff --git a/app/www/library/examples/avatar_group.py b/app/www/library/examples/avatar_group.py new file mode 100644 index 0000000..dc5c5e5 --- /dev/null +++ b/app/www/library/examples/avatar_group.py @@ -0,0 +1,26 @@ +import reflex as rx + +from components.ui.avatar import avatar + + +def avatar_as_group() -> rx.Component: + return avatar.group( + avatar.root( + avatar.image( + src="/avatars/01.png", + custom_attrs={"alt": "@avatar-1"}, + ), + avatar.fallback("RD"), + ), + avatar.root( + avatar.image( + src="/avatars/02.png", + custom_attrs={"alt": "@avatar-2"}, + ), + avatar.fallback("LI"), + ), + avatar.root( + avatar.fallback("AH"), + ), + class_name="grayscale", + ) diff --git a/app/www/library/examples/avatar_group_count.py b/app/www/library/examples/avatar_group_count.py new file mode 100644 index 0000000..bea4b94 --- /dev/null +++ b/app/www/library/examples/avatar_group_count.py @@ -0,0 +1,27 @@ +import reflex as rx + +from components.ui.avatar import avatar + + +def avatar_with_group_count() -> rx.Component: + return avatar.group( + avatar.root( + avatar.fallback("AH"), + ), + avatar.root( + avatar.image( + src="/avatars/01.png", + custom_attrs={"alt": "@avatar-1"}, + ), + avatar.fallback("RD"), + ), + avatar.root( + avatar.image( + src="/avatars/02.png", + custom_attrs={"alt": "@avatar-2"}, + ), + avatar.fallback("LI"), + ), + avatar.group_count("+3"), + class_name="grayscale", + ) diff --git a/app/www/library/examples/avatar_sizes.py b/app/www/library/examples/avatar_sizes.py index c71066b..daa9d8c 100644 --- a/app/www/library/examples/avatar_sizes.py +++ b/app/www/library/examples/avatar_sizes.py @@ -1,40 +1,32 @@ import reflex as rx + from components.ui.avatar import avatar -def avatar_sizes(): - """Example showing different avatar sizes""" - return rx.box( - avatar( - src="https://avatars.githubusercontent.com/u/104714959?s=200&v=4", - alt="@reflex", - fallback="RE", - class_name="size-6", - ), - avatar( - src="https://avatars.githubusercontent.com/u/104714959?s=200&v=4", - alt="@reflex", - fallback="RE", - class_name="size-8", +def avatar_sizes() -> rx.Component: + return rx.el.div( + avatar.root( + avatar.image( + src="https://avatars.githubusercontent.com/u/84860195?v=4", + custom_attrs={"alt": "@LineIndent"}, + ), + avatar.fallback("LI"), + size="sm", ), - avatar( - src="https://avatars.githubusercontent.com/u/104714959?s=200&v=4", - alt="@reflex", - fallback="RE", - class_name="size-10", + avatar.root( + avatar.image( + src="https://avatars.githubusercontent.com/u/84860195?v=4", + custom_attrs={"alt": "@LineIndent"}, + ), + avatar.fallback("LI"), ), - avatar( - src="https://avatars.githubusercontent.com/u/104714959?s=200&v=4", - alt="@reflex", - fallback="RE", - class_name="size-12", + avatar.root( + avatar.image( + src="https://avatars.githubusercontent.com/u/84860195?v=4", + custom_attrs={"alt": "@LineIndent"}, + ), + avatar.fallback("LI"), + size="lg", ), - avatar( - src="https://avatars.githubusercontent.com/u/104714959?s=200&v=4", - alt="@reflex", - fallback="RE", - class_name="size-16", - ), - class_name="flex items-center gap-4 p-8", + class_name="flex flex-wrap items-center gap-2", ) - diff --git a/app/www/library/examples/avatar_with_badge.py b/app/www/library/examples/avatar_with_badge.py index 6ad60b5..761f094 100644 --- a/app/www/library/examples/avatar_with_badge.py +++ b/app/www/library/examples/avatar_with_badge.py @@ -1,24 +1,16 @@ import reflex as rx + from components.ui.avatar import avatar -def avatar_with_badge(): - """Example showing avatar with status badge""" - return rx.box( - rx.box( - avatar( - src="https://avatars.githubusercontent.com/u/84860195?v=4", - alt="@LineIndent", - fallback="CN", - class_name="size-12", - ), - rx.box( - class_name=( - "absolute bottom-0 right-0 size-3 rounded-full " - "bg-green-500 border-2 border-[var(--background)]" - ), - ), - class_name="relative inline-block", +def avatar_with_badge() -> rx.Component: + return avatar.root( + avatar.image( + src="https://avatars.githubusercontent.com/u/84860195?v=4", + custom_attrs={"alt": "@LineIndent"}, + ), + avatar.fallback("AH"), + avatar.badge( + class_name="bg-green-600 dark:bg-green-800", ), - class_name="p-8", ) diff --git a/app/www/library/examples/avatar_with_icon_badge.py b/app/www/library/examples/avatar_with_icon_badge.py new file mode 100644 index 0000000..fa1d34a --- /dev/null +++ b/app/www/library/examples/avatar_with_icon_badge.py @@ -0,0 +1,17 @@ +import reflex as rx + +from components.icons.hugeicon import hi +from components.ui.avatar import avatar + + +def avatar_badge_icon() -> rx.Component: + return avatar.root( + avatar.image( + src="https://avatars.githubusercontent.com/u/104714959?s=200&v=4", + custom_attrs={"alt": "Reflex Dev"}, + ), + avatar.fallback("RD"), + avatar.badge( + hi("PlusSignIcon"), + ), + ) diff --git a/app/www/library/examples/avatar_with_icon_count.py b/app/www/library/examples/avatar_with_icon_count.py new file mode 100644 index 0000000..493426a --- /dev/null +++ b/app/www/library/examples/avatar_with_icon_count.py @@ -0,0 +1,30 @@ +import reflex as rx + +from components.icons.hugeicon import hi +from components.ui.avatar import avatar + + +def avatar_group_count_icon() -> rx.Component: + return avatar.group( + avatar.root( + avatar.fallback("AH"), + ), + avatar.root( + avatar.image( + src="https://avatars.githubusercontent.com/u/104714959?s=200&v=4", + custom_attrs={"alt": "@reflex-dev"}, + ), + avatar.fallback("CN"), + ), + avatar.root( + avatar.image( + src="https://avatars.githubusercontent.com/u/84860195?v=4", + custom_attrs={"alt": "LineIndent"}, + ), + avatar.fallback("LI"), + ), + avatar.group_count( + hi("PlusSignIcon"), + ), + class_name="grayscale", + ) diff --git a/app/www/library/examples/badge_as_link.py b/app/www/library/examples/badge_as_link.py new file mode 100644 index 0000000..abf078a --- /dev/null +++ b/app/www/library/examples/badge_as_link.py @@ -0,0 +1,15 @@ +import reflex as rx + +from components.icons.hugeicon import hi +from components.ui.badge import badge + + +def badge_as_link() -> rx.Component: + return badge( + rx.el.a( + "Open Link", + hi("ArrowUpRightIcon", custom_attrs={"data-icon": "inline-end"}), + href="#link", + class_name="inline-flex items-center gap-1 text-inherit no-underline", + ), + ) diff --git a/app/www/library/examples/badge_custom_colors.py b/app/www/library/examples/badge_custom_colors.py new file mode 100644 index 0000000..7b7292d --- /dev/null +++ b/app/www/library/examples/badge_custom_colors.py @@ -0,0 +1,29 @@ +import reflex as rx + +from components.ui.badge import badge + + +def badge_custom_colors() -> rx.Component: + return rx.el.div( + badge( + "Blue", + class_name="bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-300", + ), + badge( + "Green", + class_name="bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300", + ), + badge( + "Sky", + class_name="bg-sky-50 text-sky-700 dark:bg-sky-950 dark:text-sky-300", + ), + badge( + "Purple", + class_name="bg-purple-50 text-purple-700 dark:bg-purple-950 dark:text-purple-300", + ), + badge( + "Red", + class_name="bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300", + ), + class_name="flex flex-wrap gap-2", + ) diff --git a/app/www/library/examples/badge_default.py b/app/www/library/examples/badge_default.py deleted file mode 100644 index c09afbf..0000000 --- a/app/www/library/examples/badge_default.py +++ /dev/null @@ -1,39 +0,0 @@ -import reflex as rx -from components.ui.badge import badge - - -def badge_default(): - return rx.box( - rx.box( - badge("Badge"), - badge("Secondary", variant="secondary"), - badge("Destructive", variant="destructive"), - badge("Outline", variant="outline"), - class_name="flex w-full flex-wrap gap-2", - ), - rx.box( - badge( - rx.icon(tag="badge-check"), - "Verified", - variant="secondary", - class_name="bg-blue-500 text-white dark:bg-blue-600", - ), - badge( - "8", - class_name="h-5 min-w-5 rounded-full px-1 font-mono tabular-nums", - ), - badge( - "99", - variant="destructive", - class_name="h-5 min-w-5 rounded-full px-1 font-mono tabular-nums", - ), - badge( - "20+", - variant="outline", - class_name="h-5 min-w-5 rounded-full px-1 font-mono tabular-nums", - ), - class_name="flex w-full flex-wrap gap-2", - ), - class_name="flex flex-col items-center gap-2 p-8", - ) - diff --git a/app/www/library/examples/badge_notification_count.py b/app/www/library/examples/badge_notification_count.py deleted file mode 100644 index 48da4d3..0000000 --- a/app/www/library/examples/badge_notification_count.py +++ /dev/null @@ -1,27 +0,0 @@ -import reflex as rx -from components.ui.badge import badge - - -def badge_notification_count(): - return rx.box( - badge( - "1", - class_name="h-5 min-w-5 rounded-full px-1 font-mono tabular-nums", - ), - badge( - "5", - variant="destructive", - class_name="h-5 min-w-5 rounded-full px-1 font-mono tabular-nums", - ), - badge( - "10", - variant="secondary", - class_name="h-5 min-w-5 rounded-full px-1 font-mono tabular-nums", - ), - badge( - "99+", - variant="destructive", - class_name="h-5 min-w-5 rounded-full px-1 font-mono tabular-nums", - ), - class_name="flex items-center gap-2 p-8", - ) diff --git a/app/www/library/examples/badge_status.py b/app/www/library/examples/badge_status.py deleted file mode 100644 index 8848821..0000000 --- a/app/www/library/examples/badge_status.py +++ /dev/null @@ -1,18 +0,0 @@ -import reflex as rx -from components.ui.badge import badge - - -def badge_status(): - return rx.box( - badge("New", variant="default"), - badge("Popular", variant="secondary"), - badge("Sale", variant="destructive"), - badge("Draft", variant="outline"), - badge( - rx.icon(tag="star"), - "Featured", - class_name="bg-yellow-500 text-white dark:bg-yellow-600", - ), - class_name="flex flex-wrap gap-2 p-8", - ) - diff --git a/app/www/library/examples/badge_variants.py b/app/www/library/examples/badge_variants.py new file mode 100644 index 0000000..5435217 --- /dev/null +++ b/app/www/library/examples/badge_variants.py @@ -0,0 +1,14 @@ +import reflex as rx + +from components.ui.badge import badge + + +def badge_with_variants() -> rx.Component: + return rx.el.div( + badge("Default"), + badge("Secondary", variant="secondary"), + badge("Destructive", variant="destructive"), + badge("Outline", variant="outline"), + badge("Ghost", variant="ghost"), + class_name="flex flex-wrap gap-2", + ) diff --git a/app/www/library/examples/badge_with_icon.py b/app/www/library/examples/badge_with_icon.py new file mode 100644 index 0000000..9f29fa5 --- /dev/null +++ b/app/www/library/examples/badge_with_icon.py @@ -0,0 +1,20 @@ +import reflex as rx + +from components.icons.hugeicon import hi +from components.ui.badge import badge + + +def badge_with_icon() -> rx.Component: + return rx.el.div( + badge( + hi("CheckmarkBadge01Icon", custom_attrs={"data-icon": "inline-start"}), + "Verified", + variant="secondary", + ), + badge( + "Bookmark", + hi("Bookmark02Icon", custom_attrs={"data-icon": "inline-end"}), + variant="outline", + ), + class_name="flex flex-wrap gap-2", + ) diff --git a/app/www/library/examples/badge_with_icons.py b/app/www/library/examples/badge_with_icons.py deleted file mode 100644 index 41a1cc1..0000000 --- a/app/www/library/examples/badge_with_icons.py +++ /dev/null @@ -1,32 +0,0 @@ -import reflex as rx -from components.ui.badge import badge - - -def badge_with_icons(): - return rx.box( - badge( - rx.icon(tag="check"), - "Success", - variant="secondary", - class_name="bg-green-500 text-white dark:bg-green-600", - ), - badge( - rx.icon(tag="x"), - "Error", - variant="destructive", - ), - badge( - rx.icon(tag="triangle-alert"), - "Warning", - variant="secondary", - class_name="bg-yellow-500 text-white dark:bg-yellow-600", - ), - badge( - rx.icon(tag="info"), - "Info", - variant="secondary", - class_name="bg-blue-500 text-white dark:bg-blue-600", - ), - class_name="flex flex-wrap gap-2 p-8", - ) - diff --git a/app/www/library/examples/badge_wtih_spinner.py b/app/www/library/examples/badge_wtih_spinner.py new file mode 100644 index 0000000..1b657ca --- /dev/null +++ b/app/www/library/examples/badge_wtih_spinner.py @@ -0,0 +1,20 @@ +import reflex as rx + +from components.ui.badge import badge +from components.ui.spinner import spinner + + +def badge_with_spinner() -> rx.Component: + return rx.el.div( + badge( + spinner(custom_attrs={"data-icon": "inline-start"}), + "Deleting", + variant="destructive", + ), + badge( + "Generating", + spinner(custom_attrs={"data-icon": "inline-end"}), + variant="secondary", + ), + class_name="flex flex-wrap gap-2", + ) diff --git a/app/www/library/examples/breadcrumb_basic.py b/app/www/library/examples/breadcrumb_basic.py new file mode 100644 index 0000000..e5a4752 --- /dev/null +++ b/app/www/library/examples/breadcrumb_basic.py @@ -0,0 +1,21 @@ +import reflex as rx + +from components.ui.breadcrumb import breadcrumb + + +def breadcrumb_basic() -> rx.Component: + return breadcrumb.root( + breadcrumb.list( + breadcrumb.item( + breadcrumb.link("Home", href="#"), + ), + breadcrumb.separator(), + breadcrumb.item( + breadcrumb.link("Components", href="#"), + ), + breadcrumb.separator(), + breadcrumb.item( + breadcrumb.page("Breadcrumb"), + ), + ) + ) diff --git a/app/www/library/examples/breadcrumb_basic_demo.py b/app/www/library/examples/breadcrumb_basic_demo.py deleted file mode 100644 index b05bca7..0000000 --- a/app/www/library/examples/breadcrumb_basic_demo.py +++ /dev/null @@ -1,50 +0,0 @@ -import reflex as rx -from components.ui.breadcrumb import ( - breadcrumb, - breadcrumb_ellipsis, - breadcrumb_item, - breadcrumb_link, - breadcrumb_list, - breadcrumb_page, - breadcrumb_separator, -) - - -def breadcrumb_basic_demo(): - return rx.el.div( - breadcrumb( - breadcrumb_list( - breadcrumb_item( - breadcrumb_link("Home", href="#"), - ), - breadcrumb_separator(), - breadcrumb_item( - rx.menu.root( - rx.menu.trigger( - rx.box( - breadcrumb_ellipsis(class_name="size-4"), - rx.el.span("Toggle menu", class_name="sr-only"), - class_name="flex items-center gap-1", - ), - ), - rx.menu.content( - rx.menu.item("Documentation"), - rx.menu.item("Themes"), - rx.menu.item("GitHub"), - class_name="min-w-[8rem]", - ), - ), - ), - breadcrumb_separator(), - breadcrumb_item( - breadcrumb_link("Components", href="#"), - ), - breadcrumb_separator(), - breadcrumb_item( - breadcrumb_page("Breadcrumb"), - ), - ), - ), - class_name="p-8", - ) - diff --git a/app/www/library/examples/breadcrumb_custom_separator.py b/app/www/library/examples/breadcrumb_custom_separator.py index f00e90d..10db40b 100644 --- a/app/www/library/examples/breadcrumb_custom_separator.py +++ b/app/www/library/examples/breadcrumb_custom_separator.py @@ -1,34 +1,22 @@ import reflex as rx -from components.ui.breadcrumb import ( - breadcrumb, - breadcrumb_item, - breadcrumb_link, - breadcrumb_list, - breadcrumb_page, - breadcrumb_separator, -) +from components.icons.hugeicon import hi +from components.ui.breadcrumb import breadcrumb -def breadcrumb_custom_separator(): - return rx.el.div( - breadcrumb( - breadcrumb_list( - breadcrumb_item( - breadcrumb_link("Home", href="#"), - ), - breadcrumb_separator( - rx.text("/", class_name="text-[var(--muted-foreground)]") - ), - breadcrumb_item( - breadcrumb_link("Blog", href="#"), - ), - breadcrumb_separator( - rx.text("/", class_name="text-[var(--muted-foreground)]") - ), - breadcrumb_item( - breadcrumb_page("Article"), - ), + +def breadcrumb_custom_separator() -> rx.Component: + return breadcrumb.root( + breadcrumb.list( + breadcrumb.item( + breadcrumb.link("Home", href="#"), + ), + breadcrumb.separator(hi("LinerIcon")), + breadcrumb.item( + breadcrumb.link("Components", href="#"), + ), + breadcrumb.separator(hi("LinerIcon")), + breadcrumb.item( + breadcrumb.page("Breadcrumb"), ), - ), - class_name="p-8", + ) ) diff --git a/app/www/library/examples/breadcrumb_drop_down.py b/app/www/library/examples/breadcrumb_drop_down.py new file mode 100644 index 0000000..ea0e101 --- /dev/null +++ b/app/www/library/examples/breadcrumb_drop_down.py @@ -0,0 +1,48 @@ +import reflex as rx + +from components.icons.hugeicon import hi +from components.ui.breadcrumb import breadcrumb +from components.ui.button import button +from components.ui.menu import menu + + +def breadcrumb_dropdown_demo() -> rx.Component: + return breadcrumb.root( + breadcrumb.list( + breadcrumb.item( + breadcrumb.link("Home", href="#"), + ), + breadcrumb.separator(hi("LinerIcon")), + breadcrumb.item( + menu.root( + menu.trigger( + render_=rx.el.button( + "Components", + hi( + "ArrowDown01Icon", + custom_attrs={"data-icon": "inline-end"}, + class_name="size-4", + ), + class_name="flex flex-row items-center gap-x-2", + ) + ), + menu.portal( + menu.positioner( + menu.popup( + menu.group( + menu.item("Documentation"), + menu.item("Themes"), + menu.item("GitHub"), + ), + align="start", + ) + ), + ), + ), + ), + breadcrumb.separator(hi("LinerIcon")), + breadcrumb.item( + breadcrumb.page("Breadcrumb"), + ), + ) + ) diff --git a/app/www/library/examples/breadcrumb_ellipsis.py b/app/www/library/examples/breadcrumb_ellipsis.py new file mode 100644 index 0000000..46c29e1 --- /dev/null +++ b/app/www/library/examples/breadcrumb_ellipsis.py @@ -0,0 +1,25 @@ +import reflex as rx + +from components.ui.breadcrumb import breadcrumb + + +def breadcrumb_ellipsis() -> rx.Component: + return breadcrumb.root( + breadcrumb.list( + breadcrumb.item( + breadcrumb.link("Home", href="#"), + ), + breadcrumb.separator(), + breadcrumb.item( + breadcrumb.ellipsis(), + ), + breadcrumb.separator(), + breadcrumb.item( + breadcrumb.link("Components", href="#"), + ), + breadcrumb.separator(), + breadcrumb.item( + breadcrumb.page("Breadcrumb"), + ), + ) + ) diff --git a/app/www/library/examples/breadcrumb_icon_breadcrumb.py b/app/www/library/examples/breadcrumb_icon_breadcrumb.py deleted file mode 100644 index ea36234..0000000 --- a/app/www/library/examples/breadcrumb_icon_breadcrumb.py +++ /dev/null @@ -1,45 +0,0 @@ -import reflex as rx -from components.ui.breadcrumb import ( - breadcrumb, - breadcrumb_item, - breadcrumb_link, - breadcrumb_list, - breadcrumb_page, - breadcrumb_separator, -) - - -def breadcrumb_icon_breadcrumb(): - return rx.el.div( - breadcrumb( - breadcrumb_list( - breadcrumb_item( - breadcrumb_link( - rx.icon(tag="home", size=14), - "Home", - href="#", - class_name="flex flex-row gap-x-1 items-center", - ), - ), - breadcrumb_separator(), - breadcrumb_item( - breadcrumb_link( - rx.icon(tag="folder", size=14), - "Documents", - href="#", - class_name="flex flex-row gap-x-1 items-center", - ), - ), - breadcrumb_separator(), - breadcrumb_item( - breadcrumb_page( - rx.icon(tag="file-text", size=14), - "README.md", - class_name="flex flex-row gap-x-1 items-center", - ), - ), - ), - ), - class_name="p-8", - ) - diff --git a/app/www/library/examples/breadcrumb_simple_breadcrumb.py b/app/www/library/examples/breadcrumb_simple_breadcrumb.py deleted file mode 100644 index f1e0354..0000000 --- a/app/www/library/examples/breadcrumb_simple_breadcrumb.py +++ /dev/null @@ -1,35 +0,0 @@ -import reflex as rx -from components.ui.breadcrumb import ( - breadcrumb, - breadcrumb_item, - breadcrumb_link, - breadcrumb_list, - breadcrumb_page, - breadcrumb_separator, -) - - -def breadcrumb_simple_breadcrumb(): - return rx.box( - breadcrumb( - breadcrumb_list( - breadcrumb_item( - breadcrumb_link("Home", href="#"), - ), - breadcrumb_separator(), - breadcrumb_item( - breadcrumb_link("Products", href="#"), - ), - breadcrumb_separator(), - breadcrumb_item( - breadcrumb_link("Electronics", href="#"), - ), - breadcrumb_separator(), - breadcrumb_item( - breadcrumb_page("Laptop"), - ), - ), - ), - class_name="p-8", - ) - diff --git a/app/www/library/examples/button_as_link.py b/app/www/library/examples/button_as_link.py new file mode 100644 index 0000000..57a2611 --- /dev/null +++ b/app/www/library/examples/button_as_link.py @@ -0,0 +1,11 @@ +import reflex as rx + +from components.ui.button import button_variants + + +def button_render() -> rx.Component: + return rx.el.a( + "Login", + href="#", + class_name=button_variants(variant="secondary", size="sm"), + ) diff --git a/app/www/library/examples/button_default.py b/app/www/library/examples/button_default.py index a701f28..51b8015 100644 --- a/app/www/library/examples/button_default.py +++ b/app/www/library/examples/button_default.py @@ -2,4 +2,4 @@ def button_default(): - return button("Default", variant="default", size="sm") + return button("Button") diff --git a/app/www/library/examples/button_icon.py b/app/www/library/examples/button_icon.py index a2d9312..9f76f4b 100644 --- a/app/www/library/examples/button_icon.py +++ b/app/www/library/examples/button_icon.py @@ -4,5 +4,7 @@ def button_icon(): return button( - hi("Mail01Icon", class_name="size-4"), variant="outline", size="icon-sm" + hi("CircleArrowUp01Icon", class_name="size-4"), + variant="outline", + size="icon", ) diff --git a/app/www/library/examples/button_loading.py b/app/www/library/examples/button_loading.py new file mode 100644 index 0000000..9ad8a43 --- /dev/null +++ b/app/www/library/examples/button_loading.py @@ -0,0 +1,21 @@ +import reflex as rx + +from components.ui.button import button +from components.ui.spinner import spinner + + +def button_loading() -> rx.Component: + return rx.el.div( + button( + spinner(custom_attrs={"data-icon": "inline-start"}), + "Generating", + variant="outline", + ), + button( + "Downloading", + spinner(custom_attrs={"data-icon": "inline-end"}), + variant="secondary", + disabled=True, + ), + class_name="flex gap-2", + ) diff --git a/app/www/library/examples/button_rounded.py b/app/www/library/examples/button_rounded.py new file mode 100644 index 0000000..1777593 --- /dev/null +++ b/app/www/library/examples/button_rounded.py @@ -0,0 +1,20 @@ +import reflex as rx + +from components.icons.hugeicon import hi +from components.ui.button import button + + +def button_rounded() -> rx.Component: + return rx.el.div( + button( + "Get Started", + class_name="rounded-full", + ), + button( + hi("ArrowUp02Icon"), + variant="outline", + size="icon", + class_name="rounded-full", + ), + class_name="flex gap-2", + ) diff --git a/app/www/library/examples/button_secondary.py b/app/www/library/examples/button_secondary.py index 6b54bce..9b095f2 100644 --- a/app/www/library/examples/button_secondary.py +++ b/app/www/library/examples/button_secondary.py @@ -2,4 +2,4 @@ def button_secondary(): - return button("Secondary", variant="secondary", size="sm") + return button("Secondary", variant="secondary") diff --git a/app/www/library/examples/button_sizes.py b/app/www/library/examples/button_sizes.py index 3caa341..ebe92a1 100644 --- a/app/www/library/examples/button_sizes.py +++ b/app/www/library/examples/button_sizes.py @@ -1,12 +1,50 @@ import reflex as rx + +from components.icons.hugeicon import hi from components.ui.button import button -def button_sizes(): +def button_size() -> rx.Component: return rx.el.div( - button("Small", size="sm"), - button("Default", size="default"), - button("Large", size="lg"), - class_name="flex items-center gap-3", + rx.el.div( + button("Extra Small", size="xs", variant="outline"), + button( + hi("ArrowUpRight03Icon"), + size="icon-xs", + aria_label="Submit", + variant="outline", + ), + class_name="flex items-start gap-2", + ), + rx.el.div( + button("Small", size="sm", variant="outline"), + button( + hi("ArrowUpRight03Icon"), + size="icon-sm", + aria_label="Submit", + variant="outline", + ), + class_name="flex items-start gap-2", + ), + rx.el.div( + button("Default", variant="outline"), + button( + hi("ArrowUpRight03Icon"), + size="icon", + aria_label="Submit", + variant="outline", + ), + class_name="flex items-start gap-2", + ), + rx.el.div( + button("Large", variant="outline", size="lg"), + button( + hi("ArrowUpRight03Icon"), + size="icon-lg", + aria_label="Submit", + variant="outline", + ), + class_name="flex items-start gap-2", + ), + class_name="flex flex-col items-start gap-8 sm:flex-row", ) - diff --git a/app/www/library/examples/button_with_icon.py b/app/www/library/examples/button_with_icon.py new file mode 100644 index 0000000..c282c66 --- /dev/null +++ b/app/www/library/examples/button_with_icon.py @@ -0,0 +1,20 @@ +import reflex as rx + +from components.icons.hugeicon import hi +from components.ui.button import button + + +def button_with_icon() -> rx.Component: + return rx.el.div( + button( + hi("GitBranchIcon", custom_attrs={"data-icon": "inline-start"}), + "New Branch", + variant="outline", + ), + button( + "Fork", + hi("GitForkIcon", custom_attrs={"data-icon": "inline-end"}), + variant="outline", + ), + class_name="flex gap-2", + ) diff --git a/app/www/library/examples/scroll_area_horizontal.py b/app/www/library/examples/scroll_area_horizontal.py index 0dcc5de..55b70aa 100644 --- a/app/www/library/examples/scroll_area_horizontal.py +++ b/app/www/library/examples/scroll_area_horizontal.py @@ -49,5 +49,5 @@ def scroll_area_horizontal(): ), scroll_area.scrollbar(scroll_area.thumb(), orientation="horizontal"), scroll_area.corner(), - class_name="w-96 rounded-radius border border-input", + class_name="w-96 rounded-lg border border-input", ) diff --git a/app/www/library/examples/skeleton_card.py b/app/www/library/examples/skeleton_card.py index 40e6739..8af7be2 100644 --- a/app/www/library/examples/skeleton_card.py +++ b/app/www/library/examples/skeleton_card.py @@ -11,5 +11,5 @@ def skeleton_card(): card.content( skeleton_component(class_name="aspect-video w-full rounded-md"), ), - class_name="w-full max-w-xs border border-input rounded-radius", + class_name="w-full max-w-xs border border-input rounded-lg", ) diff --git a/app/www/wrapper.py b/app/www/wrapper.py index 6e94afe..1a791fc 100644 --- a/app/www/wrapper.py +++ b/app/www/wrapper.py @@ -94,7 +94,7 @@ def file_codeblock(file_path: str, source: str) -> rx.Component: id=f"code-panel-{toggle_height_id}", class_name="scrollbar-none flex-1 min-h-0 flex flex-col h-full relative overflow-x-auto", ), - class_name="rounded-radius outline outline-input flex-1 min-h-0 flex flex-col bg-secondary dark:bg-card", + class_name="rounded-lg outline outline-input flex-1 min-h-0 flex flex-col bg-secondary dark:bg-card", ) @@ -166,7 +166,7 @@ def file_codeblock_full(file_path: str, source: str) -> rx.Component: }, class_name="scrollbar-none flex-1 min-h-0 flex flex-col h-full", ), - class_name="rounded-radius outline outline-input flex-1 min-h-0 flex flex-col bg-secondary dark:bg-card", + class_name="rounded-lg outline outline-input flex-1 min-h-0 flex flex-col bg-secondary dark:bg-card", ) @@ -194,7 +194,7 @@ def chart_util_wrapper(source: str): ), class_name="bg-secondary dark:bg-card relative overflow-hidden", ), - class_name="w-full border border-input rounded-radius mb-8 !overflow-hidden", + class_name="w-full border border-input rounded-lg mb-8 !overflow-hidden", ), ) diff --git a/assets/docs/components/accordion.md b/assets/docs/components/accordion.md index deb99e5..f37f8a9 100644 --- a/assets/docs/components/accordion.md +++ b/assets/docs/components/accordion.md @@ -17,8 +17,6 @@ buridan add component accordion ### Manual Installation ```python -"""Custom Accordion component.""" - from typing import Any, Literal from reflex.components.component import Component, ComponentNamespace @@ -51,8 +49,6 @@ class ClassNames: class AccordionBaseComponent(BaseUIComponent): - """Base component for accordion components.""" - library = f"{PACKAGE_NAME}/accordion" @property @@ -66,34 +62,24 @@ class AccordionRoot(AccordionBaseComponent): tag = "Accordion.Root" - # The uncontrolled value of the item(s) that should be initially expanded. To render a controlled accordion, use the `value` prop instead. default_value: Var[list[Any]] - # The controlled value of the item(s) that should be expanded. To render an uncontrolled accordion, use the `default_value` prop instead. value: Var[list[Any]] - # Event handler called when an accordion item is expanded or collapsed. Provides the new value as an argument. on_value_change: EventHandler[passthrough_event_spec(list[str])] - # Allows the browser's built-in page search to find and expand the panel contents. Overrides the `keep_mounted` prop and uses `hidden="until-found"` to hide the element without removing it from the DOM. Defaults to False. hidden_until_found: Var[bool] - # Whether multiple items can be open at the same time. Defaults to True. multiple: Var[bool] - # Whether the component should ignore user interaction. Defaults to False. disabled: Var[bool] - # Whether to loop keyboard focus back to the first item when the end of the list is reached while using the arrow keys. Defaults to True. loop_focus: Var[bool] - # The visual orientation of the accordion. Controls whether roving focus uses left/right or up/down arrow keys. Defaults to 'vertical'. orientation: Var[LiteralOrientation] - # Whether to keep the element in the DOM while the panel is closed. This prop is ignored when hidden_until_found is used. Defaults to False. keep_mounted: Var[bool] - # The render prop. render_: Var[Component] @classmethod @@ -105,20 +91,14 @@ class AccordionRoot(AccordionBaseComponent): class AccordionItem(AccordionBaseComponent): - """Groups an accordion header with the corresponding panel.""" - tag = "Accordion.Item" - # The value that identifies this item. value: Var[str] - # Event handler called when the panel is opened or closed. on_open_change: EventHandler[passthrough_event_spec(bool)] - # Whether the component should ignore user interaction. Defaults to False. disabled: Var[bool] - # The render prop. render_: Var[Component] @classmethod @@ -130,11 +110,8 @@ class AccordionItem(AccordionBaseComponent): class AccordionHeader(AccordionBaseComponent): - """A heading that labels the corresponding panel.""" - tag = "Accordion.Header" - # The render prop. render_: Var[Component] @classmethod @@ -146,13 +123,13 @@ class AccordionHeader(AccordionBaseComponent): class AccordionTrigger(AccordionBaseComponent): - """A button that opens and closes the corresponding panel.""" - tag = "Accordion.Trigger" title: Var[str] + native_button: Var[bool] - render_: Var[Component] = None + + render_: Var[Component] | None = None @classmethod def create(cls, *children, **props) -> BaseUIComponent: @@ -191,17 +168,12 @@ class AccordionTrigger(AccordionBaseComponent): class AccordionPanel(AccordionBaseComponent): - """A collapsible panel with the accordion item contents.""" - tag = "Accordion.Panel" - # Allows the browser's built-in page search to find and expand the panel contents. Overrides the `keep_mounted` prop and uses `hidden="until-found"` to hide the element without removing it from the DOM. Defaults to False. hidden_until_found: Var[bool] - # Whether to keep the element in the DOM while the panel is closed. This prop is ignored when `hidden_until_found` is used. Defaults to False. keep_mounted: Var[bool] - # The render prop. render_: Var[Component] @classmethod @@ -212,132 +184,13 @@ class AccordionPanel(AccordionBaseComponent): return super().create(*children, **props) -class HighLevelAccordion(AccordionRoot): - """High level wrapper for the Accordion component.""" - - items: Var[ITEMS_TYPE] | ITEMS_TYPE - - _item_props = {"on_open_change", "disabled"} - _trigger_props = {"native_button"} - _panel_props = {"hidden_until_found", "keep_mounted"} - - @classmethod - def create( - cls, - items: Var[ITEMS_TYPE] | ITEMS_TYPE, - **props, - ) -> BaseUIComponent: - """Create a high level accordion component. - - Args: - items: List of dictionaries with 'trigger', 'content', and optional 'value' and 'disabled' keys. - **props: Additional properties to apply to the accordion component. - - Returns: - The accordion component with all necessary subcomponents. - """ - # Extract props for different parts - item_props = {k: props.pop(k) for k in cls._item_props & props.keys()} - trigger_props = {k: props.pop(k) for k in cls._trigger_props & props.keys()} - panel_props = {k: props.pop(k) for k in cls._panel_props & props.keys()} - - if isinstance(items, Var): - accordion_items = foreach( - items, - lambda item: cls._create_accordion_item_dynamic( - item, item_props, trigger_props, panel_props - ), - ) - return AccordionRoot.create(accordion_items, **props) - accordion_items = [ - cls._create_accordion_item( - item, index, item_props, trigger_props, panel_props - ) - for index, item in enumerate(items) - ] - return AccordionRoot.create(*accordion_items, **props) - - @classmethod - def _create_trigger_icon(cls) -> Component: - """Create the accordion trigger icon.""" - return icon( - "PlusSignIcon", - class_name=ClassNames.TRIGGER_ICON, - data_slot="accordion-trigger-icon", - ) - - @classmethod - def _create_accordion_item( - cls, - item: dict[str, str | Component], - index: int, - item_props: dict, - trigger_props: dict, - panel_props: dict, - ) -> BaseUIComponent: - """Create a single accordion item from a dictionary (for normal lists).""" - return AccordionItem.create( - AccordionHeader.create( - AccordionTrigger.create( - item.get("trigger"), - cls._create_trigger_icon(), - **trigger_props, - ), - ), - AccordionPanel.create( - Div.create( - item.get("content"), - class_name=ClassNames.PANEL_DIV, - data_slot="accordion-panel-div", - ), - **panel_props, - ), - value=item.get("value", f"item-{index + 1}"), - disabled=item.get("disabled", False), - **item_props, - ) - - @classmethod - def _create_accordion_item_dynamic( - cls, - item: ObjectVar[dict[str, str | Component]], - item_props: dict, - trigger_props: dict, - panel_props: dict, - ) -> BaseUIComponent: - """Create a single accordion item from a dictionary (for Var items).""" - return AccordionItem.create( - AccordionHeader.create( - AccordionTrigger.create( - item["trigger"], - cls._create_trigger_icon(), - **trigger_props, - ), - ), - AccordionPanel.create( - Div.create( - item["content"], - class_name=ClassNames.PANEL_DIV, - data_slot="accordion-panel-div", - ), - **panel_props, - ), - value=item.get("value", ""), - disabled=item.get("disabled", False).bool(), - **item_props, - ) - - class Accordion(ComponentNamespace): - """Namespace for Accordion components.""" - root = staticmethod(AccordionRoot.create) item = staticmethod(AccordionItem.create) header = staticmethod(AccordionHeader.create) trigger = staticmethod(AccordionTrigger.create) panel = staticmethod(AccordionPanel.create) class_names = ClassNames - __call__ = staticmethod(HighLevelAccordion.create) accordion = Accordion() @@ -348,7 +201,7 @@ accordion = Accordion() ```python -from components.ui.accordion import accordion +from components.ui.accordion import Accordion ``` diff --git a/assets/docs/components/attachment.md b/assets/docs/components/attachment.md index d826589..1353fb7 100644 --- a/assets/docs/components/attachment.md +++ b/assets/docs/components/attachment.md @@ -17,8 +17,6 @@ buridan add component attachment ### Manual Installation ```python -"""Attachment component — file and image attachment previews for chat UI.""" - from typing import Literal import reflex as rx @@ -43,7 +41,7 @@ class ClassNames: "data-[state=idle]:border-dashed" ) - SIZES: dict[str, str] = { + SIZES = { "default": ( "gap-2 text-sm " "has-data-[slot=attachment-content]:px-2.5 " @@ -64,7 +62,7 @@ class ClassNames: ), } - ORIENTATIONS: dict[str, str] = { + ORIENTATIONS = { "horizontal": "min-w-40 items-center", "vertical": "w-24 flex-col has-data-[slot=attachment-content]:w-30", } @@ -84,7 +82,7 @@ class ClassNames: "group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5" ) - MEDIA_VARIANTS: dict[str, str] = { + MEDIA_VARIANTS = { "icon": "", "image": ( "opacity-60 " @@ -136,13 +134,7 @@ def attachment_root( class_name: str = "", **props, ) -> rx.Component: - """ - Root attachment container. - orientation: horizontal (default) | vertical - size: default | sm | xs - state: idle | uploading | processing | error | done (default) - """ return rx.el.div( *children, data_slot="attachment", @@ -165,7 +157,7 @@ def attachment_media( class_name: str = "", **props, ) -> rx.Component: - """Icon or image preview slot.""" + return rx.el.div( *children, data_slot="attachment-media", @@ -180,7 +172,7 @@ def attachment_media( def attachment_content(*children, class_name: str = "", **props) -> rx.Component: - """Text content area — holds title and description.""" + return rx.el.div( *children, data_slot="attachment-content", @@ -190,7 +182,7 @@ def attachment_content(*children, class_name: str = "", **props) -> rx.Component def attachment_title(*children, class_name: str = "", **props) -> rx.Component: - """Filename or attachment title. Shimmers during uploading/processing.""" + return rx.el.span( *children, data_slot="attachment-title", @@ -200,7 +192,7 @@ def attachment_title(*children, class_name: str = "", **props) -> rx.Component: def attachment_description(*children, class_name: str = "", **props) -> rx.Component: - """File type, size, or other metadata.""" + return rx.el.span( *children, data_slot="attachment-description", @@ -210,10 +202,7 @@ def attachment_description(*children, class_name: str = "", **props) -> rx.Compo def attachment_actions(*children, class_name: str = "", **props) -> rx.Component: - """ - Row of action buttons. - In vertical orientation, absolutely positioned top-right. - """ + return rx.el.div( *children, data_slot="attachment-actions", @@ -223,9 +212,10 @@ def attachment_actions(*children, class_name: str = "", **props) -> rx.Component def attachment_action(*children, class_name: str = "", **props) -> rx.Component: - """Individual action button.""" props.setdefault("variant", "ghost") + props.setdefault("size", "icon-xs") + return button( *children, data_slot="attachment-action", @@ -250,10 +240,7 @@ def attachment_trigger( def attachment_group(*children, class_name: str = "", **props) -> rx.Component: - """ - Horizontal scrolling row of attachments. - Snaps to each attachment on scroll. - """ + return rx.el.div( *children, data_slot="attachment-group", @@ -263,8 +250,6 @@ def attachment_group(*children, class_name: str = "", **props) -> rx.Component: class Attachment(ComponentNamespace): - """Attachment namespace.""" - root = staticmethod(attachment_root) media = staticmethod(attachment_media) content = staticmethod(attachment_content) diff --git a/assets/docs/components/autocomplete.md b/assets/docs/components/autocomplete.md index 4503aa2..09e01ee 100644 --- a/assets/docs/components/autocomplete.md +++ b/assets/docs/components/autocomplete.md @@ -19,8 +19,6 @@ buridan add component autocomplete ### Manual Installation ```python -"""Custom autocomplete component with flat and grouped item support.""" - from typing import Any from reflex.components.component import Component @@ -417,23 +415,35 @@ function BuridanAutocompleteRoot({ class BuridanAutocomplete(Component): tag = "BuridanAutocompleteRoot" + is_default = False items: Var[Any] + placeholder: Var[str] + root_class: Var[str] + input_class: Var[str] + popup_class: Var[str] + list_class: Var[str] + item_class: Var[str] + item_highlighted_class: Var[str] + empty_class: Var[str] + group_label_class: Var[str] + separator_class: Var[str] set_query_on_select: Var[bool] = True on_change_query: EventHandler[passthrough_event_spec(str)] + on_select_item: EventHandler[passthrough_event_spec(str)] def add_custom_code(self) -> list[str]: diff --git a/assets/docs/components/avatar.md b/assets/docs/components/avatar.md index 2af408a..847acbe 100644 --- a/assets/docs/components/avatar.md +++ b/assets/docs/components/avatar.md @@ -17,129 +17,128 @@ buridan add component avatar ### Manual Installation ```python -"""Custom avatar component.""" - from reflex.components.component import Component, ComponentNamespace from reflex.event import EventHandler, passthrough_event_spec from reflex.utils.imports import ImportVar from reflex.vars.base import Var +from reflex_components_core.el import Div, Span +from ..utils.twmerge import cn from .base_ui import PACKAGE_NAME, BaseUIComponent +from .component import CoreComponent class ClassNames: - """Class names for avatar components.""" - - ROOT = "shrink-0 inline-flex size-8 items-center justify-center overflow-hidden rounded-full align-middle text-base font-medium select-none" - IMAGE = "size-full object-cover shrink-0" - FALLBACK = "flex size-full items-center justify-center text-sm bg-muted" + ROOT = "group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=xl]:size-14 data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten" + IMAGE = "aspect-square size-full rounded-full object-cover" + FALLBACK = "flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs" + BADGE = "absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2 group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2" + GROUP = "group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background" + GROUP_COUNT = "relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3" class AvatarBaseComponent(BaseUIComponent): - """Base component for avatar components.""" - library = f"{PACKAGE_NAME}/avatar" @property def import_var(self): - """Return the import variable for the avatar component.""" return ImportVar(tag="Avatar", package_path="", install=False) class AvatarRoot(AvatarBaseComponent): - """Displays a user's profile picture, initials, or fallback icon.""" - tag = "Avatar.Root" - - # The component to render render_: Var[Component] @classmethod def create(cls, *children, **props) -> BaseUIComponent: - """Create the avatar root component.""" + custom_classes = props.pop("class_name", "") + size = props.pop("size", "default") props["data-slot"] = "avatar" - cls.set_class_name(ClassNames.ROOT, props) - return super().create(*children, **props) + props["data-size"] = size + + return super().create( + *children, class_name=cn(ClassNames.ROOT, custom_classes), **props + ) class AvatarImage(AvatarBaseComponent): - """The image to be displayed in the avatar.""" - tag = "Avatar.Image" - - # The image source URL src: Var[str] - - # Callback when loading status changes on_loading_status_change: EventHandler[passthrough_event_spec(str)] - - # The component to render render_: Var[Component] @classmethod def create(cls, *children, **props) -> BaseUIComponent: - """Create the avatar image component.""" + custom_classes = props.pop("class_name", "") props["data-slot"] = "avatar-image" - cls.set_class_name(ClassNames.IMAGE, props) - return super().create(*children, **props) + + return super().create( + *children, class_name=cn(ClassNames.IMAGE, custom_classes), **props + ) class AvatarFallback(AvatarBaseComponent): - """Rendered when the image fails to load or when no image is provided.""" - tag = "Avatar.Fallback" - - # How long to wait before showing the fallback. Specified in milliseconds delay: Var[int] - - # The component to render render_: Var[Component] @classmethod def create(cls, *children, **props) -> BaseUIComponent: - """Create the avatar fallback component.""" + custom_classes = props.pop("class_name", "") props["data-slot"] = "avatar-fallback" - cls.set_class_name(ClassNames.FALLBACK, props) - return super().create(*children, **props) + return super().create( + *children, + class_name=cn(ClassNames.FALLBACK, custom_classes), + **props, + ) -class HighLevelAvatar(AvatarRoot): - """High level wrapper for the Avatar component.""" - # The image source URL - src: Var[str] +class AvatarBadge(Span, CoreComponent): + @classmethod + def create(cls, *children, **props) -> Span: + custom_classes = props.pop("class_name", "") + props["data-slot"] = "avatar-badge" + + return super().create( + *children, class_name=cn(ClassNames.BADGE, custom_classes), **props + ) - # Image props - _image_props = {"src", "on_loading_status_change", "render_"} - # Fallback props - _fallback_props = {"delay"} +class AvatarGroup(Div, CoreComponent): + """The flex container wrapper for grouping multiple avatars.""" @classmethod - def create(cls, *children, **props) -> BaseUIComponent: - """Create the avatar component.""" - # Extract props for each subcomponent - image_props = {k: props.pop(k) for k in cls._image_props & props.keys()} - fallback_props = {k: props.pop(k) for k in cls._fallback_props & props.keys()} + def create(cls, *children, **props) -> Div: + custom_classes = props.pop("class_name", "") + props["data-slot"] = "avatar-group" - fallback_content = props.pop("fallback", "") + return super().create( + *children, class_name=cn(ClassNames.GROUP, custom_classes), **props + ) + + +class AvatarGroupCount(Div, CoreComponent): + @classmethod + def create(cls, *children, **props) -> Div: + custom_classes = props.pop("class_name", "") + props["data-slot"] = "avatar-group-count" - return AvatarRoot.create( - AvatarImage.create(**image_props), - AvatarFallback.create(fallback_content, **fallback_props), + return super().create( *children, + class_name=cn(ClassNames.GROUP_COUNT, custom_classes), **props, ) class Avatar(ComponentNamespace): - """Namespace for Avatar components.""" - root = staticmethod(AvatarRoot.create) image = staticmethod(AvatarImage.create) fallback = staticmethod(AvatarFallback.create) + badge = staticmethod(AvatarBadge.create) + group = staticmethod(AvatarGroup.create) + group_count = staticmethod(AvatarGroupCount.create) class_names = ClassNames - __call__ = staticmethod(HighLevelAvatar.create) avatar = Avatar() @@ -150,7 +149,7 @@ avatar = Avatar() ```python -from components.ui.avatar import avatar +from components.ui.avatar import Avatar ``` @@ -166,125 +165,288 @@ avatar.root( ``` - # Examples +## Basic + +A basic avatar component with an image and a fallback. -## General -Displays a basic avatar with either a user image or a fallback placeholder. +```python +def avatar_basic() -> rx.Component: + return avatar.root( + avatar.image( + src="https://github.com/LineIndent.png", + custom_attrs={"alt": "@lineindent"}, + ), + avatar.fallback("AH"), + ) +``` + + +## Badge + +Use the `avatar.badge` component to add a badge to the avatar. The badge is positioned at the bottom right of the avatar. ```python -def avatar_general(): - return rx.box( - avatar( +def avatar_with_badge() -> rx.Component: + return avatar.root( + avatar.image( src="https://avatars.githubusercontent.com/u/84860195?v=4", - alt="@LineIndent", - fallback="CN", + custom_attrs={"alt": "@LineIndent"}, ), - avatar( - src="https://avatars.githubusercontent.com/u/198465274?s=200&v=4", - alt="@buridan-ui", - fallback="BUI", - class_name="rounded-lg", + avatar.fallback("AH"), + avatar.badge( + class_name="bg-green-600 dark:bg-green-800", ), - rx.box( - avatar( - src="", - alt="@buridan-ui", - fallback="BU", + ) +``` + + +Use the `class_Name` prop to add custom styles to the badge such as custom colors, sizes, etc. + +```reflex +avatar.badge(class_name="bg-green-600 dark:bg-green-800") +``` + +## Badge with Icon + +You can also use an icon inside `avatar.badge`. + + +```python +def avatar_badge_icon() -> rx.Component: + return avatar.root( + avatar.image( + src="https://avatars.githubusercontent.com/u/104714959?s=200&v=4", + custom_attrs={"alt": "Reflex Dev"}, + ), + avatar.fallback("RD"), + avatar.badge( + hi("PlusSignIcon"), + ), + ) +``` + + +## Avatar Group + +Use the `avatar.group` component to add a group of avatars. + + +```python +def avatar_as_group() -> rx.Component: + return avatar.group( + avatar.root( + avatar.image( + src="/avatars/01.png", + custom_attrs={"alt": "@avatar-1"}, ), - avatar( - src="https://avatars.githubusercontent.com/u/84860195?v=4", - alt="@buridan-ui", - fallback="BUI", + avatar.fallback("RD"), + ), + avatar.root( + avatar.image( + src="/avatars/02.png", + custom_attrs={"alt": "@avatar-2"}, ), - avatar( - src="https://avatars.githubusercontent.com/u/104714959?s=200&v=4", - alt="@reflex", - fallback="RE", + avatar.fallback("LI"), + ), + avatar.root( + avatar.fallback("AH"), + ), + class_name="grayscale", + ) +``` + + +## Avatar Group Count + +Use `avatar.group_count` to add a count to the group. + + +```python +def avatar_with_group_count() -> rx.Component: + return avatar.group( + avatar.root( + avatar.fallback("AH"), + ), + avatar.root( + avatar.image( + src="/avatars/01.png", + custom_attrs={"alt": "@avatar-1"}, ), - class_name=( - "flex -space-x-2 " - "*:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-[var(--background)] " - "*:data-[slot=avatar]:grayscale" + avatar.fallback("RD"), + ), + avatar.root( + avatar.image( + src="/avatars/02.png", + custom_attrs={"alt": "@avatar-2"}, ), + avatar.fallback("LI"), ), - class_name="flex flex-row flex-wrap items-center gap-12 p-8", + avatar.group_count("+3"), + class_name="grayscale", ) ``` -## Sizes +## Avatar Group with Icon -Demonstrates how to scale the avatar component using Tailwind utility classes. +You can also use an icon inside `avatar.group_count`. ```python -def avatar_sizes(): - """Example showing different avatar sizes""" - return rx.box( - avatar( - src="https://avatars.githubusercontent.com/u/104714959?s=200&v=4", - alt="@reflex", - fallback="RE", - class_name="size-6", +def avatar_group_count_icon() -> rx.Component: + return avatar.group( + avatar.root( + avatar.fallback("AH"), ), - avatar( - src="https://avatars.githubusercontent.com/u/104714959?s=200&v=4", - alt="@reflex", - fallback="RE", - class_name="size-8", - ), - avatar( - src="https://avatars.githubusercontent.com/u/104714959?s=200&v=4", - alt="@reflex", - fallback="RE", - class_name="size-10", + avatar.root( + avatar.image( + src="https://avatars.githubusercontent.com/u/104714959?s=200&v=4", + custom_attrs={"alt": "@reflex-dev"}, + ), + avatar.fallback("CN"), ), - avatar( - src="https://avatars.githubusercontent.com/u/104714959?s=200&v=4", - alt="@reflex", - fallback="RE", - class_name="size-12", + avatar.root( + avatar.image( + src="https://avatars.githubusercontent.com/u/84860195?v=4", + custom_attrs={"alt": "LineIndent"}, + ), + avatar.fallback("LI"), ), - avatar( - src="https://avatars.githubusercontent.com/u/104714959?s=200&v=4", - alt="@reflex", - fallback="RE", - class_name="size-16", + avatar.group_count( + hi("PlusSignIcon"), ), - class_name="flex items-center gap-4 p-8", + class_name="grayscale", ) ``` -## With Badge +## Sizes -Shows how to combine an avatar with status or notification badges for added context. +Use the `size` prop to change the size of the avatar. ```python -def avatar_with_badge(): - """Example showing avatar with status badge""" - return rx.box( - rx.box( - avatar( +def avatar_sizes() -> rx.Component: + return rx.el.div( + avatar.root( + avatar.image( src="https://avatars.githubusercontent.com/u/84860195?v=4", - alt="@LineIndent", - fallback="CN", - class_name="size-12", + custom_attrs={"alt": "@LineIndent"}, ), - rx.box( - class_name=( - "absolute bottom-0 right-0 size-3 rounded-full " - "bg-green-500 border-2 border-[var(--background)]" - ), + avatar.fallback("LI"), + size="sm", + ), + avatar.root( + avatar.image( + src="https://avatars.githubusercontent.com/u/84860195?v=4", + custom_attrs={"alt": "@LineIndent"}, + ), + avatar.fallback("LI"), + ), + avatar.root( + avatar.image( + src="https://avatars.githubusercontent.com/u/84860195?v=4", + custom_attrs={"alt": "@LineIndent"}, ), - class_name="relative inline-block", + avatar.fallback("LI"), + size="lg", ), - class_name="p-8", + class_name="flex flex-wrap items-center gap-2", ) ``` + +## Dropdown + +You can use the `Avatar` component as a trigger for a dropdown menu. + + +```python +def avatar_dropdown_menu() -> rx.Component: + return menu.root( + menu.trigger( + render_=button( + avatar.root( + avatar.image( + src="https://github.com/LineIndent.png", + custom_attrs={"alt": "lineindent"}, + ), + avatar.fallback("LI"), + ), + variant="ghost", + size="icon", + class_name="rounded-full", + ) + ), + menu.portal( + menu.positioner( + menu.popup( + menu.group( + menu.item("Profile"), + menu.item("Billing"), + menu.item("Settings"), + ), + class_name="w-32", + ), + ) + ), + ) +``` + + +# API Reference + +## avatar.root + +The `avatar.root` component is the root component that wraps the avatar image and fallback. + +| Prop | Type | Default | +| ----------- | --------------------------- | ----------- | +| `size` | `"default" \| "sm" \| "lg"` | `"default"` | +| `class_name` | `string` | - | + +## avatar.image + +The `avatar.image` component displays the avatar image. It accepts all Base UI Avatar Image props. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `src` | `string` | - | +| `alt` | `string` | - | +| `class_name` | `string` | - | + +## avatar.fallback + +The `avatar.fallback` component displays a fallback when the image fails to load. It accepts all Base UI Avatar Fallback props. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | + +## avatar.badge + +The `avatar.badge` component displays a badge indicator on the avatar, typically positioned at the bottom right. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | + +## avatar.group + +The `avatar.group` component displays a group of avatars with overlapping styling. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | + +## avatar.group_count + +The `avatar.group_count` component displays a count indicator in an avatar group, typically showing the number of additional avatars. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | diff --git a/assets/docs/components/badge.md b/assets/docs/components/badge.md index f4a8bf2..788d841 100644 --- a/assets/docs/components/badge.md +++ b/assets/docs/components/badge.md @@ -22,86 +22,54 @@ from typing import Literal from reflex.vars.base import Var from reflex_components_core.el import Span +from ..utils.twmerge import cn from .component import CoreComponent -LiteralBadgeVariant = Literal["default", "secondary", "destructive", "outline"] +LiteralBadgeVariant = Literal[ + "default", "secondary", "destructive", "outline", "ghost", "link" +] DEFAULT_BASE_CLASSES = ( - "inline-flex items-center justify-center rounded-radius border px-2 py-0.5 text-xs font-medium " - "w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none " - "focus-visible:border-[var(--ring)] focus-visible:ring-[var(--ring)]/50 focus-visible:ring-[3px] " - "aria-invalid:ring-[var(--destructive)]/20 dark:aria-invalid:ring-[var(--destructive)]/40 " - "aria-invalid:border-[var(--destructive)] transition-[color,box-shadow] overflow-hidden" + "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden " + "rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap " + "transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 " + "has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive " + "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!" ) BADGE_VARIANTS = { - "default": ( - "border-transparent bg-[var(--primary)] text-[var(--primary-foreground)] " - "[a&]:hover:bg-[var(--primary)]/90" - ), - "secondary": ( - "border-transparent bg-[var(--secondary)] text-[var(--secondary-foreground)] " - "[a&]:hover:bg-[var(--secondary)]/90" - ), + "default": "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + "secondary": "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", "destructive": ( - "border-transparent bg-[var(--destructive)] text-white " - "[a&]:hover:bg-[var(--destructive)]/90 " - "focus-visible:ring-[var(--destructive)]/20 dark:focus-visible:ring-[var(--destructive)]/40 " - "dark:bg-[var(--destructive)]/60" - ), - "outline": ( - "text-[var(--foreground)] border-[var(--input)] " - "[a&]:hover:bg-[var(--accent)] [a&]:hover:text-[var(--accent-foreground)]" + "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 " + "dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20" ), + "outline": "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", + "ghost": "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", + "link": "text-primary underline-offset-4 hover:underline", } -def get_badge_classes(variant: LiteralBadgeVariant) -> str: - """Get the complete badge class string. - - Args: - variant: The badge variant to apply - - Returns: - The complete class string for the badge - """ - variant_classes = BADGE_VARIANTS[variant] - return f"{DEFAULT_BASE_CLASSES} {variant_classes}" +def badge_variants(variant: str = "default") -> Var: + return cn( + DEFAULT_BASE_CLASSES, + BADGE_VARIANTS.get(variant, BADGE_VARIANTS["default"]), + ) class Badge(Span, CoreComponent): - """A badge component that displays a label.""" - - # Badge variant - variant: Var[LiteralBadgeVariant] - @classmethod def create(cls, *children, **props) -> Span: - """Create the badge component. - - Args: - *children: The badge content - **props: Component properties including variant - - Returns: - A configured Span component - """ + custom_classes = props.pop("class_name", "") variant = props.pop("variant", "default") + props["data-slot"] = "badge" + props["data-variant"] = variant - cls.set_class_name(get_badge_classes(variant), props) - - # Add data-slot attribute - props.setdefault("data_slot", "badge") - - return super().create(*children, **props) - - def _exclude_props(self) -> list[str]: - """Exclude component-specific props from being passed to the DOM. - - Returns: - List of prop names to exclude - """ - return [*super()._exclude_props(), "variant"] + return super().create( + *children, + class_name=cn(badge_variants(variant), custom_classes), + **props, + ) badge = Badge.create @@ -125,139 +93,131 @@ badge() ``` - # Examples +## Variants -## Default - -Displays a standard badge using the default variant, ideal for basic labeling. +Use the `variant` prop to change the variant of the badge. ```python -def badge_default(): - return rx.box( - rx.box( - badge("Badge"), - badge("Secondary", variant="secondary"), - badge("Destructive", variant="destructive"), - badge("Outline", variant="outline"), - class_name="flex w-full flex-wrap gap-2", - ), - rx.box( - badge( - rx.icon(tag="badge-check"), - "Verified", - variant="secondary", - class_name="bg-blue-500 text-white dark:bg-blue-600", - ), - badge( - "8", - class_name="h-5 min-w-5 rounded-full px-1 font-mono tabular-nums", - ), - badge( - "99", - variant="destructive", - class_name="h-5 min-w-5 rounded-full px-1 font-mono tabular-nums", - ), - badge( - "20+", - variant="outline", - class_name="h-5 min-w-5 rounded-full px-1 font-mono tabular-nums", - ), - class_name="flex w-full flex-wrap gap-2", - ), - class_name="flex flex-col items-center gap-2 p-8", +def badge_with_variants() -> rx.Component: + return rx.el.div( + badge("Default"), + badge("Secondary", variant="secondary"), + badge("Destructive", variant="destructive"), + badge("Outline", variant="outline"), + badge("Ghost", variant="ghost"), + class_name="flex flex-wrap gap-2", ) ``` ## With Icons -Demonstrates how to include icons inside badges for visual context or emphasis. +You can render an icon inside the badge. Use `data-icon="inline-start"` to render the icon on the left and `data-icon="inline-end"` to render the icon on the right. ```python -def badge_with_icons(): - return rx.box( +def badge_with_icon() -> rx.Component: + return rx.el.div( badge( - rx.icon(tag="check"), - "Success", + hi("CheckmarkBadge01Icon", custom_attrs={"data-icon": "inline-start"}), + "Verified", variant="secondary", - class_name="bg-green-500 text-white dark:bg-green-600", ), badge( - rx.icon(tag="x"), - "Error", - variant="destructive", + "Bookmark", + hi("Bookmark02Icon", custom_attrs={"data-icon": "inline-end"}), + variant="outline", ), + class_name="flex flex-wrap gap-2", + ) +``` + + +## With Spinner + +You can render a spinner inside the badge. Remember to add the `data-icon="inline-start"` or `data-icon="inline-end"` prop to the spinner. + + +```python +def badge_with_spinner() -> rx.Component: + return rx.el.div( badge( - rx.icon(tag="triangle-alert"), - "Warning", - variant="secondary", - class_name="bg-yellow-500 text-white dark:bg-yellow-600", + spinner(custom_attrs={"data-icon": "inline-start"}), + "Deleting", + variant="destructive", ), badge( - rx.icon(tag="info"), - "Info", + "Generating", + spinner(custom_attrs={"data-icon": "inline-end"}), variant="secondary", - class_name="bg-blue-500 text-white dark:bg-blue-600", ), - class_name="flex flex-wrap gap-2 p-8", + class_name="flex flex-wrap gap-2", ) ``` -## Status +## Link -Showcases how badges can represent different statuses, like success or error, using color. +You can pass in `rx.el.a` to turn a badge into a link. The `badge` component accepts `*children` so any interactive element can be passed to it. ```python -def badge_status(): - return rx.box( - badge("New", variant="default"), - badge("Popular", variant="secondary"), - badge("Sale", variant="destructive"), - badge("Draft", variant="outline"), - badge( - rx.icon(tag="star"), - "Featured", - class_name="bg-yellow-500 text-white dark:bg-yellow-600", +def badge_as_link() -> rx.Component: + return badge( + rx.el.a( + "Open Link", + hi("ArrowUpRightIcon", custom_attrs={"data-icon": "inline-end"}), + href="#link", + class_name="inline-flex items-center gap-1 text-inherit no-underline", ), - class_name="flex flex-wrap gap-2 p-8", ) ``` -## Notification Count +## Custom Colors -Illustrates how to use badges for showing counts, such as unread notifications or messages. +You can customize the colors of a badge by adding custom classes such as `bg-green-50 dark:bg-green-800` to the `badge` component. ```python -def badge_notification_count(): - return rx.box( +def badge_custom_colors() -> rx.Component: + return rx.el.div( badge( - "1", - class_name="h-5 min-w-5 rounded-full px-1 font-mono tabular-nums", + "Blue", + class_name="bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-300", ), badge( - "5", - variant="destructive", - class_name="h-5 min-w-5 rounded-full px-1 font-mono tabular-nums", + "Green", + class_name="bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300", ), badge( - "10", - variant="secondary", - class_name="h-5 min-w-5 rounded-full px-1 font-mono tabular-nums", + "Sky", + class_name="bg-sky-50 text-sky-700 dark:bg-sky-950 dark:text-sky-300", ), badge( - "99+", - variant="destructive", - class_name="h-5 min-w-5 rounded-full px-1 font-mono tabular-nums", + "Purple", + class_name="bg-purple-50 text-purple-700 dark:bg-purple-950 dark:text-purple-300", + ), + badge( + "Red", + class_name="bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300", ), - class_name="flex items-center gap-2 p-8", + class_name="flex flex-wrap gap-2", ) ``` + + +# API Reference + +## badge + +The `badge` component displays a badge or a component that looks like a badge. + +| Prop | Type | Default | +| ----------- | ----------------------------------------------------------------------------- | ----------- | +| `variant` | `"default" \| "secondary" \| "destructive" \| "outline" \| "ghost" \| "link"` | `"default"` | +| `class_name` | `string` | - | diff --git a/assets/docs/components/breadcrumb.md b/assets/docs/components/breadcrumb.md index bc7daf1..78b9721 100644 --- a/assets/docs/components/breadcrumb.md +++ b/assets/docs/components/breadcrumb.md @@ -19,99 +19,139 @@ buridan add component breadcrumb ```python import reflex as rx +from ..icons.hugeicon import hi +from ..utils.twmerge import cn -def breadcrumb(*children, **props): - """Breadcrumb navigation container""" - return rx.el.nav( - *children, aria_label="breadcrumb", data_slot="breadcrumb", **props - ) +class Breadcrumb: + @classmethod + def root(cls, *children, **props) -> rx.Component: + custom_classes = props.pop("class_name", "") -def breadcrumb_list(*children, class_name: str = "", **props): - """Ordered list container for breadcrumb items""" - base_classes = ( - "text-[var(--muted-foreground)] flex flex-wrap items-center gap-1 text-sm " - "break-words sm:gap-2.5" - ) + props["aria-label"] = "breadcrumb" - return rx.el.ol( - *children, - data_slot="breadcrumb-list", - class_name=f"{base_classes} {class_name}".strip(), - **props, - ) + props["data-slot"] = "breadcrumb" + return rx.el.nav( + *children, + class_name=cn(custom_classes), + **props, + ) -def breadcrumb_item(*children, class_name: str = "", **props): - """Individual breadcrumb item""" - base_classes = "inline-flex items-center gap-1.5" + @classmethod + def list(cls, *children, **props) -> rx.Component: + custom_classes = props.pop("class_name", "") - return rx.el.li( - *children, - data_slot="breadcrumb-item", - class_name=f"{base_classes} {class_name}".strip(), - **props, - ) + props["data-slot"] = "breadcrumb-list" + base_classes = ( + "flex flex-wrap items-center gap-1.5 text-sm wrap-break-word " + "text-muted-foreground" + ) -def breadcrumb_link(*children, href: str = "#", class_name: str = "", **props): - """Breadcrumb link (clickable)""" - base_classes = "hover:text-[var(--foreground)] transition-colors no-underline" + return rx.el.ol( + *children, + class_name=cn(base_classes, custom_classes), + **props, + ) - return rx.el.a( - *children, - href=href, - data_slot="breadcrumb-link", - class_name=f"{base_classes} {class_name}".strip(), - **props, - ) + @classmethod + def item(cls, *children, **props) -> rx.Component: + custom_classes = props.pop("class_name", "") + props["data-slot"] = "breadcrumb-item" -def breadcrumb_page(*children, class_name: str = "", **props): - """Current page breadcrumb (non-clickable)""" - base_classes = "text-[var(--foreground)] font-normal" + base_classes = "inline-flex items-center gap-1" - return rx.el.span( - *children, - role="link", - aria_disabled="true", - aria_current="page", - data_slot="breadcrumb-page", - class_name=f"{base_classes} {class_name}".strip(), - **props, - ) + return rx.el.li( + *children, + class_name=cn(base_classes, custom_classes), + **props, + ) + @classmethod + def link(cls, *children, **props) -> rx.Component: + custom_classes = props.pop("class_name", "") -def breadcrumb_separator(*children, class_name: str = "", **props): - """Separator between breadcrumb items""" - base_classes = "[&>svg]:size-3.5" + props["data-slot"] = "breadcrumb-link" - if not children: - children = (rx.icon(tag="chevron-right", size=14),) + props.setdefault("href", "#") - return rx.el.li( - *children, - role="presentation", - aria_hidden="true", - data_slot="breadcrumb-separator", - class_name=f"{base_classes} {class_name}".strip(), - **props, - ) + base_classes = "transition-colors hover:text-foreground no-underline" + return rx.el.a( + *children, + class_name=cn(base_classes, custom_classes), + **props, + ) -def breadcrumb_ellipsis(class_name: str = "", **props): - """Ellipsis for collapsed breadcrumb items""" - base_classes = "flex size-9 items-center justify-center" + @classmethod + def page(cls, *children, **props) -> rx.Component: + custom_classes = props.pop("class_name", "") - return rx.el.span( - rx.icon(tag="ellipsis", size=16), - rx.el.span("More", class_name="sr-only"), - role="presentation", - aria_hidden="true", - data_slot="breadcrumb-ellipsis", - class_name=f"{base_classes} {class_name}".strip(), - **props, - ) + props["data-slot"] = "breadcrumb-page" + + props["role"] = "link" + + props["aria-disabled"] = "true" + + props["aria-current"] = "page" + + base_classes = "font-normal text-foreground" + + return rx.el.span( + *children, + class_name=cn(base_classes, custom_classes), + **props, + ) + + @classmethod + def separator(cls, *children, **props) -> rx.Component: + custom_classes = props.pop("class_name", "") + + props["data-slot"] = "breadcrumb-separator" + + props["role"] = "presentation" + + props["aria-hidden"] = "true" + + base_classes = "[&>svg]:size-3.5" + + if not children: + children = (hi("ArrowRight01Icon"),) + + return rx.el.li( + *children, + class_name=cn(base_classes, custom_classes), + **props, + ) + + @classmethod + def ellipsis(cls, *children, **props) -> rx.Component: + custom_classes = props.pop("class_name", "") + + props["data-slot"] = "breadcrumb-ellipsis" + + props["role"] = "presentation" + + props["aria-hidden"] = "true" + + base_classes = "flex size-5 items-center justify-center [&>svg]:size-4" + + if not children: + children = ( + rx.icon(tag="ellipsis"), + rx.el.span("More", class_name="sr-only"), + ) + + return rx.el.span( + *children, + class_name=cn(base_classes, custom_classes), + **props, + ) + + +breadcrumb = Breadcrumb ``` @@ -137,150 +177,187 @@ breadcrumb( # Examples -## Basic Demo -A basic breadcrumb showing the default navigation structure. +## Basic +A basic breadcrumb with a home link and a components link. ```python -def breadcrumb_basic_demo(): - return rx.el.div( - breadcrumb( - breadcrumb_list( - breadcrumb_item( - breadcrumb_link("Home", href="#"), - ), - breadcrumb_separator(), - breadcrumb_item( - rx.menu.root( - rx.menu.trigger( - rx.box( - breadcrumb_ellipsis(class_name="size-4"), - rx.el.span("Toggle menu", class_name="sr-only"), - class_name="flex items-center gap-1", - ), - ), - rx.menu.content( - rx.menu.item("Documentation"), - rx.menu.item("Themes"), - rx.menu.item("GitHub"), - class_name="min-w-[8rem]", - ), - ), - ), - breadcrumb_separator(), - breadcrumb_item( - breadcrumb_link("Components", href="#"), - ), - breadcrumb_separator(), - breadcrumb_item( - breadcrumb_page("Breadcrumb"), - ), +def breadcrumb_basic() -> rx.Component: + return breadcrumb.root( + breadcrumb.list( + breadcrumb.item( + breadcrumb.link("Home", href="#"), ), - ), - class_name="p-8", + breadcrumb.separator(), + breadcrumb.item( + breadcrumb.link("Components", href="#"), + ), + breadcrumb.separator(), + breadcrumb.item( + breadcrumb.page("Breadcrumb"), + ), + ) ) ``` -## Simple Breadcrumb -A minimal breadcrumb with plain text links. +## Custom Separator +Use a custom component as `children` for `breadcrumb.separator` to create a custom separator. ```python -def breadcrumb_simple_breadcrumb(): - return rx.box( - breadcrumb( - breadcrumb_list( - breadcrumb_item( - breadcrumb_link("Home", href="#"), - ), - breadcrumb_separator(), - breadcrumb_item( - breadcrumb_link("Products", href="#"), - ), - breadcrumb_separator(), - breadcrumb_item( - breadcrumb_link("Electronics", href="#"), - ), - breadcrumb_separator(), - breadcrumb_item( - breadcrumb_page("Laptop"), - ), +def breadcrumb_custom_separator() -> rx.Component: + return breadcrumb.root( + breadcrumb.list( + breadcrumb.item( + breadcrumb.link("Home", href="#"), ), - ), - class_name="p-8", + breadcrumb.separator(hi("LinerIcon")), + breadcrumb.item( + breadcrumb.link("Components", href="#"), + ), + breadcrumb.separator(hi("LinerIcon")), + breadcrumb.item( + breadcrumb.page("Breadcrumb"), + ), + ) ) ``` -## Icon Breadcrumb -A breadcrumb that includes icons alongside link labels. +## Dropdown + +You can compose `breadcrumb.item` with a `menu.root` to create a dropdown in the breadcrumb. ```python -def breadcrumb_icon_breadcrumb(): - return rx.el.div( - breadcrumb( - breadcrumb_list( - breadcrumb_item( - breadcrumb_link( - rx.icon(tag="home", size=14), - "Home", - href="#", - class_name="flex flex-row gap-x-1 items-center", - ), - ), - breadcrumb_separator(), - breadcrumb_item( - breadcrumb_link( - rx.icon(tag="folder", size=14), - "Documents", - href="#", - class_name="flex flex-row gap-x-1 items-center", +def breadcrumb_dropdown_demo() -> rx.Component: + return breadcrumb.root( + breadcrumb.list( + breadcrumb.item( + breadcrumb.link("Home", href="#"), + ), + breadcrumb.separator(hi("LinerIcon")), + breadcrumb.item( + menu.root( + menu.trigger( + render_=rx.el.button( + "Components", + hi( + "ArrowDown01Icon", + custom_attrs={"data-icon": "inline-end"}, + class_name="size-4", + ), + class_name="flex flex-row items-center gap-x-2", + ) ), - ), - breadcrumb_separator(), - breadcrumb_item( - breadcrumb_page( - rx.icon(tag="file-text", size=14), - "README.md", - class_name="flex flex-row gap-x-1 items-center", + menu.portal( + menu.positioner( + menu.popup( + menu.group( + menu.item("Documentation"), + menu.item("Themes"), + menu.item("GitHub"), + ), + align="start", + ) + ), ), ), ), - ), - class_name="p-8", + breadcrumb.separator(hi("LinerIcon")), + breadcrumb.item( + breadcrumb.page("Breadcrumb"), + ), + ) ) ``` -## Custom Separator -A breadcrumb with a customized separator between items. +## Collapsed + +We provide a `breadcrumb.ellipsis` component to show a collapsed state when the breadcrumb is too long. ```python -def breadcrumb_custom_separator(): - return rx.el.div( - breadcrumb( - breadcrumb_list( - breadcrumb_item( - breadcrumb_link("Home", href="#"), - ), - breadcrumb_separator( - rx.text("/", class_name="text-[var(--muted-foreground)]") - ), - breadcrumb_item( - breadcrumb_link("Blog", href="#"), - ), - breadcrumb_separator( - rx.text("/", class_name="text-[var(--muted-foreground)]") - ), - breadcrumb_item( - breadcrumb_page("Article"), - ), +def breadcrumb_ellipsis() -> rx.Component: + return breadcrumb.root( + breadcrumb.list( + breadcrumb.item( + breadcrumb.link("Home", href="#"), ), - ), - class_name="p-8", + breadcrumb.separator(), + breadcrumb.item( + breadcrumb.ellipsis(), + ), + breadcrumb.separator(), + breadcrumb.item( + breadcrumb.link("Components", href="#"), + ), + breadcrumb.separator(), + breadcrumb.item( + breadcrumb.page("Breadcrumb"), + ), + ) ) ``` + +# API Reference + +## breadcrumb.root + +The `breadcrumb.root` component is the root navigation element that wraps all breadcrumb components. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | + +## breadcrumb.list + +The `breadcrumb.list` component displays the ordered list of breadcrumb items. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | + +## breadcrumb.item + +The `breadcrumb.item` component wraps individual breadcrumb items. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | + +## breadcrumb.link + +The `breadcrumb.link` component displays a clickable link in the breadcrumb. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | + +## breadcrumb.page + +The `breadcrumb.page` component displays the current page in the breadcrumb (non-clickable). + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | + +## breadcrumb.separator + +The `breadcrumb.separator` component displays a separator between breadcrumb items. You can pass custom children to override the default separator icon. + +| Prop | Type | Default | +| ----------- | ----------------- | ------- | +| `children` | `rx.Component` | - | +| `class_name` | `string` | - | + +## breadcrumb.ellipsis + +The `breadcrumb.ellipsis` component displays an ellipsis indicator for collapsed breadcrumb items. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | diff --git a/assets/docs/components/button.md b/assets/docs/components/button.md index c093a36..02668cf 100644 --- a/assets/docs/components/button.md +++ b/assets/docs/components/button.md @@ -20,121 +20,139 @@ buridan add component button from typing import Literal from reflex.vars.base import Var -from reflex_components_core.core import cond from reflex_components_core.el import Button as BaseButton -from ..icons.others import spinner +from ..utils.twmerge import cn from .component import CoreComponent LiteralButtonVariant = Literal[ - "primary", "destructive", "outline", "secondary", "ghost", "link", "dark" + "default", + "destructive", + "outline", + "secondary", + "ghost", + "link", ] LiteralButtonSize = Literal[ - "default", "xs", "sm", "lg", "icon", "icon-xs", "icon-sm", "icon-lg" + "default", + "xs", + "sm", + "lg", + "icon", + "icon-xs", + "icon-sm", + "icon-lg", ] DEFAULT_CLASS_NAME = ( - "font-theme inline-flex items-center justify-center gap-2 whitespace-nowrap " - "rounded-radius text-sm font-medium transition-all " - "disabled:pointer-events-none disabled:opacity-50 outline-none " - "[&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 " - "[&_svg]:shrink-0 shrink-0" + "group/button inline-flex shrink-0 items-center justify-center " + "rounded-lg border border-transparent bg-clip-padding " + "text-sm font-medium whitespace-nowrap transition-all outline-none select-none " + "focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 " + "active:not-aria-[haspopup]:translate-y-px " + "disabled:pointer-events-none disabled:opacity-50 " + "aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 " + "dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 " + "[&_svg]:pointer-events-none [&_svg]:shrink-0 " + "[&_svg:not([class*='size-'])]:size-4" ) BUTTON_VARIANTS = { "variant": { - "default": "bg-primary text-primary-foreground hover:bg-primary/90", + "default": ("bg-primary text-primary-foreground hover:bg-primary/80"), + "outline": ( + "border-border bg-background hover:bg-muted hover:text-foreground " + "aria-expanded:bg-muted aria-expanded:text-foreground " + "dark:border-input dark:bg-input/30 dark:hover:bg-input/50" + ), + "secondary": ( + "bg-secondary text-secondary-foreground " + "hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] " + "aria-expanded:bg-secondary aria-expanded:text-secondary-foreground" + ), + "ghost": ( + "hover:bg-muted hover:text-foreground " + "aria-expanded:bg-muted aria-expanded:text-foreground " + "dark:hover:bg-muted/50" + ), "destructive": ( "bg-destructive/10 text-destructive hover:bg-destructive/20 " "focus-visible:border-destructive/40 focus-visible:ring-destructive/20 " - "dark:bg-destructive/20 dark:hover:bg-destructive/30" - ), - "outline": ( - "border border-input bg-background shadow-xs text-foreground " - "hover:bg-accent hover:text-accent-foreground " - "dark:bg-input/30 dark:hover:bg-input/50" + "dark:bg-destructive/20 dark:hover:bg-destructive/30 " + "dark:focus-visible:ring-destructive/40" ), - "secondary": "bg-secondary text-secondary-foreground hover:bg-secondary/80", - "ghost": "hover:bg-accent hover:text-accent-foreground dark:hover:bg-muted/50", "link": "text-primary underline-offset-4 hover:underline", }, "size": { - "default": "h-8 gap-1.5 px-2.5 has-[>svg]:px-2.5", - "xs": "h-6 rounded-[min(calc(var(--radius) * 0.8),10px)] gap-1 px-2 text-xs", - "sm": "h-7 rounded-[min(calc(var(--radius) * 0.8),12px)] gap-1 px-2.5 text-[0.8rem]", - "lg": "h-9 gap-1.5 px-2.5", + "default": ( + "h-8 gap-1.5 px-2.5 " + "has-data-[icon=inline-end]:pr-2 " + "has-data-[icon=inline-start]:pl-2" + ), + "xs": ( + "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs " + "in-data-[slot=button-group]:rounded-lg " + "has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 " + "[&_svg:not([class*='size-'])]:size-3" + ), + "sm": ( + "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] " + "in-data-[slot=button-group]:rounded-lg " + "has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 " + "[&_svg:not([class*='size-'])]:size-3.5" + ), + "lg": ( + "h-9 gap-1.5 px-2.5 " + "has-data-[icon=inline-end]:pr-2 " + "has-data-[icon=inline-start]:pl-2" + ), "icon": "size-8", - "icon-xs": "size-6 rounded-[min(calc(var(--radius) * 0.8),10px)]", - "icon-sm": "size-7 rounded-[min(calc(var(--radius) * 0.8),12px)]", + "icon-xs": ( + "size-6 rounded-[min(var(--radius-md),10px)] " + "in-data-[slot=button-group]:rounded-lg " + "[&_svg:not([class*='size-'])]:size-3" + ), + "icon-sm": ( + "size-7 rounded-[min(var(--radius-md),12px)] " + "in-data-[slot=button-group]:rounded-lg" + ), "icon-lg": "size-9", }, } class Button(BaseButton, CoreComponent): - """A custom button component.""" - - # Button variant. Defaults to "primary". variant: Var[LiteralButtonVariant] - - # Button size. Defaults to "md". size: Var[LiteralButtonSize] - # The loading state of the button - loading: Var[bool] - @classmethod def create(cls, *children, **props) -> BaseButton: - """Create the button component.""" variant = props.pop("variant", "default") - cls.validate_variant(variant) - size = props.pop("size", "default") - cls.validate_size(size) - - loading = props.pop("loading", False) - disabled = props.pop("disabled", False) - - button_classes = f"{DEFAULT_CLASS_NAME} {BUTTON_VARIANTS['variant'][variant]} {BUTTON_VARIANTS['size'][size]}" - - cls.set_class_name(button_classes, props) - - children_list = list(children) - - if isinstance(loading, Var): - props["disabled"] = cond(loading, True, disabled) - children_list.insert(0, cond(loading, spinner())) - else: - props["disabled"] = True if loading else disabled - children_list.insert(0, spinner()) if loading else None - - return super().create(*children_list, **props) + custom_classes = props.pop("class_name", "") + + return super().create( + *children, + data_slot="button", + class_name=cn( + DEFAULT_CLASS_NAME, + BUTTON_VARIANTS["variant"].get(variant, ""), + BUTTON_VARIANTS["size"].get(size, ""), + custom_classes, + ), + **props, + ) - @staticmethod - def validate_variant(variant: LiteralButtonVariant): - """Validate the button variant.""" - if variant not in BUTTON_VARIANTS["variant"]: - available_variants = ", ".join(BUTTON_VARIANTS["variant"].keys()) - message = ( - f"Invalid variant: {variant}. Available variants: {available_variants}" - ) - raise ValueError(message) + def _exclude_props(self) -> list[str]: + return [*super()._exclude_props(), "size", "variant"] - @staticmethod - def validate_size(size: LiteralButtonSize): - """Validate the button size.""" - if size not in BUTTON_VARIANTS["size"]: - available_sizes = ", ".join(BUTTON_VARIANTS["size"].keys()) - message = f"Invalid size: {size}. Available sizes: {available_sizes}" - raise ValueError(message) - def _exclude_props(self) -> list[str]: - return [ - *super()._exclude_props(), - "size", - "variant", - "loading", - ] +def button_variants(variant: str = "default", size: str = "default") -> Var: + return cn( + DEFAULT_CLASS_NAME, + BUTTON_VARIANTS["variant"].get(variant, ""), + BUTTON_VARIANTS["size"].get(size, ""), + ) button = Button.create @@ -158,52 +176,81 @@ button() ``` - # Examples - ## Sizes -Showcases buttons in different predefined sizes (default, small, large, icon, etc). +Use the `size` prop to change the size of the button. ```python -def button_sizes(): +def button_size() -> rx.Component: return rx.el.div( - button("Small", size="sm"), - button("Default", size="default"), - button("Large", size="lg"), - class_name="flex items-center gap-3", + rx.el.div( + button("Extra Small", size="xs", variant="outline"), + button( + hi("ArrowUpRight03Icon"), + size="icon-xs", + aria_label="Submit", + variant="outline", + ), + class_name="flex items-start gap-2", + ), + rx.el.div( + button("Small", size="sm", variant="outline"), + button( + hi("ArrowUpRight03Icon"), + size="icon-sm", + aria_label="Submit", + variant="outline", + ), + class_name="flex items-start gap-2", + ), + rx.el.div( + button("Default", variant="outline"), + button( + hi("ArrowUpRight03Icon"), + size="icon", + aria_label="Submit", + variant="outline", + ), + class_name="flex items-start gap-2", + ), + rx.el.div( + button("Large", variant="outline", size="lg"), + button( + hi("ArrowUpRight03Icon"), + size="icon-lg", + aria_label="Submit", + variant="outline", + ), + class_name="flex items-start gap-2", + ), + class_name="flex flex-col items-start gap-8 sm:flex-row", ) ``` ## Default -The default visual style for buttons with standard background and hover effects. - ```python def button_default(): - return button("Default", variant="default", size="sm") + return button("Button") ``` ## Secondary -A more muted alternative to the default button, useful for less prominent actions. - ```python def button_secondary(): - return button("Secondary", variant="secondary", size="sm") + return button("Secondary", variant="secondary") ``` ## Outline -Buttons with a bordered outline, blending well with minimal UIs or light themes. - ```python def button_outline(): @@ -213,8 +260,6 @@ def button_outline(): ## Ghost -A button style with no background or border, ideal for subtle UI actions. - ```python def button_ghost(): @@ -224,8 +269,6 @@ def button_ghost(): ## Link -A button styled to look like a hyperlink — useful for inline actions or navigation. - ```python def button_link(): @@ -235,8 +278,6 @@ def button_link(): ## Destructive -A bold style used for destructive or dangerous actions like “Delete”. - ```python def button_destructive(): @@ -246,13 +287,112 @@ def button_destructive(): ## Icon -Examples showing icon-only buttons with varying sizes for compact UI elements. - ```python def button_icon(): return button( - hi("Mail01Icon", class_name="size-4"), variant="outline", size="icon-sm" + hi("CircleArrowUp01Icon", class_name="size-4"), + variant="outline", + size="icon", ) ``` + +## With Icon + +Remember to add the `data-icon="inline-start"` or `data-icon="inline-end"` attribute to the icon for the correct spacing. + + +```python +def button_with_icon() -> rx.Component: + return rx.el.div( + button( + hi("GitBranchIcon", custom_attrs={"data-icon": "inline-start"}), + "New Branch", + variant="outline", + ), + button( + "Fork", + hi("GitForkIcon", custom_attrs={"data-icon": "inline-end"}), + variant="outline", + ), + class_name="flex gap-2", + ) +``` + + +## Rounded + +Use the `rounded-full` class to make the button rounded. + + +```python +def button_rounded() -> rx.Component: + return rx.el.div( + button( + "Get Started", + class_name="rounded-full", + ), + button( + hi("ArrowUp02Icon"), + variant="outline", + size="icon", + class_name="rounded-full", + ), + class_name="flex gap-2", + ) +``` + + +## Spinner + +Render a `spinner()` component inside the button to show a loading state. Remember to add the `data-icon="inline-start"` or `data-icon="inline-end"` attribute to the spinner for the correct spacing. + + +```python +def button_loading() -> rx.Component: + return rx.el.div( + button( + spinner(custom_attrs={"data-icon": "inline-start"}), + "Generating", + variant="outline", + ), + button( + "Downloading", + spinner(custom_attrs={"data-icon": "inline-end"}), + variant="secondary", + disabled=True, + ), + class_name="flex gap-2", + ) +``` + + +## As Link + +You can use the `button_variants` helper function to make a link look like a button. + +Do **not** use `button(rx.el.a(...))` for links. The Base UI `Button` component always applies `role="button"`, which overrides the semantic link role on `` elements. Use `button_variants` with a plain `rx.el.a` tag instead to cleanly generate the necessary classes as a dynamic Reflex `Var`. + + +```python +def button_render() -> rx.Component: + return rx.el.a( + "Login", + href="#", + class_name=button_variants(variant="secondary", size="sm"), + ) +``` + + + +# API Reference + +## Button + +The `Button` component is a wrapper around the `button` element that adds a variety of styles and functionality. + +| Prop | Type | Default | +| --------- | ------------------------------------------------------------------------------------ | ----------- | +| `variant` | `"default" \| "outline" \| "ghost" \| "destructive" \| "secondary" \| "link"` | `"default"` | +| `size` | `"default" \| "xs" \| "sm" \| "lg" \| "icon" \| "icon-xs" \| "icon-sm" \| "icon-lg"` | `"default"` | diff --git a/assets/docs/components/input.md b/assets/docs/components/input.md index 75b32dc..ca8a43f 100644 --- a/assets/docs/components/input.md +++ b/assets/docs/components/input.md @@ -24,7 +24,7 @@ INPUT = ( "w-full file:text-foreground placeholder:text-muted-foreground " "selection:bg-primary selection:text-primary-foreground " "dark:bg-input/30 border-input " - "h-9 w-full min-w-0 rounded-radius border bg-transparent px-3 py-1 text-base " + "h-9 w-full min-w-0 rounded-lg border bg-transparent px-3 py-1 text-base " "transition-[color,box-shadow] outline-none " "file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium " "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 " diff --git a/assets/docs/components/menu.md b/assets/docs/components/menu.md index 45fb364..edeb2dc 100644 --- a/assets/docs/components/menu.md +++ b/assets/docs/components/menu.md @@ -17,21 +17,15 @@ buridan add component menu ### Manual Installation ```python -"""Custom menu component.""" - from typing import Literal from reflex.components.component import Component, ComponentNamespace from reflex.event import EventHandler, passthrough_event_spec from reflex.utils.imports import ImportVar from reflex.vars.base import Var -from reflex_components_core.core.foreach import foreach from ..icons.hugeicon import hi -from ..icons.others import select_arrow -from ..utils.twmerge import cn from .base_ui import PACKAGE_NAME, BaseUIComponent -from .button import button LiteralOpenChangeReason = Literal[ "arrowKey", @@ -59,7 +53,7 @@ class ClassNames: POPUP = ( "z-50 max-h-[var(--available-height)] w-[var(--anchor-width)] min-w-32 " "origin-[var(--transform-origin)] overflow-x-hidden overflow-y-auto " - "rounded-radius bg-popover p-1 text-popover-foreground shadow-md " + "rounded-lg bg-popover p-1 text-popover-foreground shadow-md " "ring-1 ring-foreground/10 duration-100 outline-none " "data-[side=bottom]:slide-in-from-top-2 " "data-[side=left]:slide-in-from-right-2 " @@ -70,7 +64,7 @@ class ClassNames: ) ITEM = ( "group/menu-item relative flex cursor-default items-center gap-1.5 " - "rounded-radius px-1.5 py-1 text-sm outline-hidden select-none " + "rounded-lg px-1.5 py-1 text-sm outline-hidden select-none " "focus:bg-accent focus:text-accent-foreground " "not-data-[variant=destructive]:focus:**:text-accent-foreground " "data-inset:pl-7 " @@ -86,7 +80,7 @@ class ClassNames: POSITIONER_SUB = "isolate z-50 outline-none" POPUP_SUB = ( "z-50 w-auto min-w-24 origin-(--transform-origin) overflow-x-hidden " - "overflow-y-auto rounded-radius bg-popover p-1 text-popover-foreground " + "overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground " "shadow-lg ring-1 ring-foreground/10 duration-100 " "data-[side=bottom]:slide-in-from-top-2 " "data-[side=left]:slide-in-from-right-2 " @@ -99,7 +93,7 @@ class ClassNames: "px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7" ) SUBMENU_TRIGGER = ( - "flex cursor-default items-center justify-between gap-1.5 rounded-radius px-1.5 py-1 " + "flex cursor-default items-center justify-between gap-1.5 rounded-lg px-1.5 py-1 " "text-sm outline-hidden select-none " "focus:bg-accent focus:text-accent-foreground " "not-data-[variant=destructive]:focus:**:text-accent-foreground " @@ -315,18 +309,6 @@ class MenuPopup(MenuBaseComponent): return super().create( *children, **props, - # style={ - # "keyframes": { - # "animate-in": { - # "from": {"opacity": "0", "transform": "scale(0.95)"}, - # "to": {"opacity": "1", "transform": "scale(1)"}, - # }, - # "animate-out": { - # "from": {"opacity": "1", "transform": "scale(2)"}, - # "to": {"opacity": "0", "transform": "scale(0.95)"}, - # }, - # }, - # }, ) @@ -637,133 +619,6 @@ class MenuSeparator(MenuBaseComponent): return super().create(*children, **props) -class HighLevelMenu(MenuRoot): - """High level wrapper for the Menu component.""" - - # The trigger component to use for the menu - trigger: Var[Component | None] - - # The list of items to display in the menu dropdown - can be strings or tuples of (label, on_click_handler) - items: Var[list[str | tuple[str, EventHandler]]] - - # The placeholder text to display when no item is selected - placeholder: Var[str] - - # The size of the menu. Defaults to "md". - size: Var[LiteralMenuSize] - - # Whether to close the menu when the item is clicked. Defaults to True. - close_on_click: Var[bool] - - # Props for different component parts - _item_props = {"close_on_click"} - _trigger_props = {"placeholder", "size", "close_on_click"} - _positioner_props = { - "align", - "align_offset", - "side", - "arrow_padding", - "collision_padding", - "sticky", - "position_method", - "track_anchor", - "side_offset", - "collision_avoidance", - } - _portal_props = {"container"} - - @classmethod - def create(cls, *children, **props) -> BaseUIComponent: - """Create a menu component. - - Args: - *children: Additional children to include in the menu. - **props: Additional properties to apply to the menu component. - - Returns: - The menu component. - """ - # Extract props for different parts - item_props = {k: props.pop(k) for k in cls._item_props & props.keys()} - trigger_props = {k: props.pop(k) for k in cls._trigger_props & props.keys()} - positioner_props = { - k: props.pop(k) for k in cls._positioner_props & props.keys() - } - portal_props = {k: props.pop(k) for k in cls._portal_props & props.keys()} - - trigger = props.pop("trigger", None) - items = props.pop("items", []) - size = trigger_props.get("size", "md") - trigger_label = trigger_props.get("placeholder", "Open Menu") - - def create_menu_item(item: str | tuple[str, EventHandler]) -> BaseUIComponent: - if isinstance(item, tuple): - label, on_click_handler = item - return MenuItem.create( - render_=button( - label, - variant="ghost", - class_name=ClassNames.ITEM, - disabled=props.get("disabled", False), - on_click=on_click_handler, - size=size, - type="button", - ), - key=label, - **item_props, - ) - return MenuItem.create( - render_=button( - item, - variant="ghost", - class_name=ClassNames.ITEM, - disabled=props.get("disabled", False), - size=size, - type="button", - ), - key=item, - **item_props, - ) - - if isinstance(items, Var): - items_children = foreach(items, create_menu_item) - else: - items_children = [create_menu_item(item) for item in items] - - return MenuRoot.create( - MenuTrigger.create( - render_=( - trigger - if trigger - else button( - trigger_label, - select_arrow(class_name="size-4 text-secondary-9"), - variant="outline", - class_name=ClassNames.TRIGGER, - disabled=props.get("disabled", False), - size=size, - type="button", - ) - ), - ), - MenuPortal.create( - MenuPositioner.create( - MenuPopup.create( - items_children, - class_name=cn( - ClassNames.POPUP, - "", - ), - ), - **positioner_props, - ), - **portal_props, - ), - *children, - **props, - ) - - class Menu(ComponentNamespace): """Namespace for Menu components.""" @@ -785,7 +640,6 @@ class Menu(ComponentNamespace): submenu_root = staticmethod(MenuSubMenuRoot.create) submenu_trigger = staticmethod(MenuSubMenuTrigger.create) class_names = ClassNames - __call__ = staticmethod(HighLevelMenu.create) menu = Menu() @@ -796,7 +650,7 @@ menu = Menu() ```python -from components.ui.menu import menu +from components.ui.menu import Menu ``` diff --git a/assets/docs/components/scroll-area.md b/assets/docs/components/scroll-area.md index 08ac94b..0949ff0 100644 --- a/assets/docs/components/scroll-area.md +++ b/assets/docs/components/scroll-area.md @@ -310,7 +310,7 @@ def scroll_area_horizontal(): ), scroll_area.scrollbar(scroll_area.thumb(), orientation="horizontal"), scroll_area.corner(), - class_name="w-96 rounded-radius border border-input", + class_name="w-96 rounded-lg border border-input", ) ``` diff --git a/assets/docs/components/select.md b/assets/docs/components/select.md index d5e9907..c383b36 100644 --- a/assets/docs/components/select.md +++ b/assets/docs/components/select.md @@ -41,7 +41,7 @@ LiteralOrientation = Literal["horizontal", "vertical"] class ClassNames: """Class names for select components.""" - TRIGGER = "flex w-fit items-center justify-between gap-1.5 rounded-radius border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" + TRIGGER = "flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" VALUE = "flex-1 text-left cursor-default" ICON = "flex size-4 text-secondary-10 group-data-[disabled]/trigger:text-current" POPUP = "relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95" @@ -590,7 +590,7 @@ class HighLevelSelect(SelectRoot): class_name=cn( ClassNames.POPUP, "", - "rounded-radius", + "rounded-lg", ), ), **positioner_props, diff --git a/assets/docs/components/skeleton.md b/assets/docs/components/skeleton.md index 0ed4941..4bfdd36 100644 --- a/assets/docs/components/skeleton.md +++ b/assets/docs/components/skeleton.md @@ -84,7 +84,7 @@ def skeleton_card(): card.content( skeleton_component(class_name="aspect-video w-full rounded-md"), ), - class_name="w-full max-w-xs border border-input rounded-radius", + class_name="w-full max-w-xs border border-input rounded-lg", ) ``` diff --git a/assets/docs/components/slider.md b/assets/docs/components/slider.md index 1647013..e9b49b1 100644 --- a/assets/docs/components/slider.md +++ b/assets/docs/components/slider.md @@ -57,19 +57,19 @@ class ClassNames: ) TRACK = ( - "h-1 w-full rounded-radius bg-secondary select-none " + "h-1 w-full rounded-lg bg-secondary select-none " "data-[orientation=vertical]:h-full " "data-[orientation=vertical]:w-1" ) INDICATOR = ( - "absolute h-full rounded-radius bg-primary select-none " + "absolute h-full rounded-lg bg-primary select-none " "data-[orientation=vertical]:w-full " "data-[orientation=vertical]:h-auto" ) THUMB = ( - "size-3 rounded-radius bg-white outline-[1px] outline-black " + "size-3 rounded-lg bg-white outline-[1px] outline-black " "select-none box-shadow:[0_0_0_1px_rgba(0,0,0,1),0_1px_2px_rgba(0,0,0,.04)] " "data-[dragging]:h-5 transition-[height,scale] hover:h-4.5" ) diff --git a/assets/docs/components/spinner.md b/assets/docs/components/spinner.md index d859d03..ac09775 100644 --- a/assets/docs/components/spinner.md +++ b/assets/docs/components/spinner.md @@ -26,6 +26,12 @@ from ..utils.twmerge import cn def spinner(class_name: str = "", **props) -> rx.Component: + incoming_attrs = props.pop("custom_attrs", {}) + + base_attrs = {"viewBox": "0 0 16 16"} + + merged_attrs = {**base_attrs, **incoming_attrs} + return svg( svg.path( opacity="0.2", @@ -39,7 +45,7 @@ def spinner(class_name: str = "", **props) -> rx.Component: stroke_width="1.5", ), xmlns="http://www.w3.org/2000/svg", - custom_attrs={"viewBox": "0 0 16 16"}, + custom_attrs=merged_attrs, class_name=cn("size-4 animate-spin fill-none", class_name), data_slot="spinner", role="status", diff --git a/assets/docs/components/tabs.md b/assets/docs/components/tabs.md index 39c8a44..8d3b0e3 100644 --- a/assets/docs/components/tabs.md +++ b/assets/docs/components/tabs.md @@ -36,7 +36,7 @@ class ClassNames: LIST = ( "relative bg-muted text-muted-foreground inline-flex h-9 w-full w-fit items-center " - "justify-center rounded-radius p-[3px] " + "justify-center rounded-lg p-[3px] " "data-[orientation=vertical]:flex data-[orientation=vertical]:flex-col " "data-[orientation=vertical]:h-auto " "data-[orientation=vertical]:p-1 data-[orientation=vertical]:gap-1" @@ -45,7 +45,7 @@ class ClassNames: "relative z-[1] flex items-center justify-center text-sm font-medium " "text-foreground dark:text-muted-foreground " "data-[selected]:text-foreground " - "rounded-radius px-2 py-1 transition-all " + "rounded-lg px-2 py-1 transition-all " "data-[orientation=vertical]:justify-start data-[orientation=vertical]:w-full " "data-[orientation=vertical]:text-left" ) @@ -55,7 +55,7 @@ class ClassNames: ) INDICATOR = ( - "absolute z-[0] rounded-radius bg-background shadow-sm dark:border dark:border-input dark:bg-input/30 " + "absolute z-[0] rounded-lg bg-background shadow-sm dark:border dark:border-input dark:bg-input/30 " "transition-all duration-200 ease-in-out " "[left:var(--active-tab-left)] [top:var(--active-tab-top)] " "[width:var(--active-tab-width)] [height:var(--active-tab-height)]" diff --git a/assets/docs/components/textarea.md b/assets/docs/components/textarea.md index 491c2be..139cba1 100644 --- a/assets/docs/components/textarea.md +++ b/assets/docs/components/textarea.md @@ -28,7 +28,7 @@ from .component import CoreComponent class ClassNames: """Class names for textarea components.""" - ROOT = "flex field-sizing-content min-h-16 min-w-xs rounded-radius border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40" + ROOT = "flex field-sizing-content min-h-16 min-w-xs rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40" class Textarea(TextareaComponent, CoreComponent): diff --git a/assets/docs/components/toggle.md b/assets/docs/components/toggle.md index b74459a..c38ce09 100644 --- a/assets/docs/components/toggle.md +++ b/assets/docs/components/toggle.md @@ -31,7 +31,7 @@ from .base_ui import PACKAGE_NAME, BaseUIComponent class ClassNames: """Class names for toggle components.""" - ROOT = "group/toggle p-1 inline-flex items-center justify-center gap-1 rounded-radius border border-input text-sm font-medium whitespace-nowrap transition-all hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" + ROOT = "group/toggle p-1 inline-flex items-center justify-center gap-1 rounded-lg border border-input text-sm font-medium whitespace-nowrap transition-all hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" ICON_VARIANT_CLASSES = { "fill": "data-[pressed]:[&_svg]:fill-primary", diff --git a/assets/docs/components/tooltip.md b/assets/docs/components/tooltip.md index bf94282..edf04f8 100644 --- a/assets/docs/components/tooltip.md +++ b/assets/docs/components/tooltip.md @@ -40,7 +40,7 @@ class ClassNames: """Class names for tooltip components.""" TRIGGER = "inline-flex items-center justify-center" - POPUP = "z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-radius bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95" + POPUP = "z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-lg bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95" ARROW = "data-[side=bottom]:top-[-7.5px] data-[side=left]:right-[-12.5px] data-[side=left]:rotate-90 data-[side=right]:left-[-12.5px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-7.5px] data-[side=top]:rotate-180" diff --git a/assets/docs/resources/theming.md b/assets/docs/resources/theming.md index ffb4726..ff0dc26 100644 --- a/assets/docs/resources/theming.md +++ b/assets/docs/resources/theming.md @@ -69,7 +69,7 @@ Make sure to pull your CSS files where your tokens are defined in your `rx.App` app = rx.App(stylesheets=["globals.css"]) ``` -Tailwind maps these tokens into utilities like `bg-background`, `text-foreground`, `border-border`, and `rounded-radius`. Dark mode works by overriding the same tokens inside a `.dark` selector. +Tailwind maps these tokens into utilities like `bg-background`, `text-foreground`, `border-border`, and `rounded-lg`. Dark mode works by overriding the same tokens inside a `.dark` selector. # Theme Tokens diff --git a/assets/globals.css b/assets/globals.css index 9f21ccf..d3a7c77 100644 --- a/assets/globals.css +++ b/assets/globals.css @@ -83,6 +83,16 @@ body { --token-keyword: oklch(0.58 0.22 27); } +@theme inline { + --radius-sm: calc(var(--radius) * 0.6); + --radius-md: calc(var(--radius) * 0.8); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) * 1.4); + --radius-2xl: calc(var(--radius) * 1.8); + --radius-3xl: calc(var(--radius) * 2.2); + --radius-4xl: calc(var(--radius) * 2.6); +} + .toc-link[data-active="true"] { color: var(--foreground); font-weight: 500; diff --git a/components/ui/accordion.py b/components/ui/accordion.py index e926791..669abc9 100644 --- a/components/ui/accordion.py +++ b/components/ui/accordion.py @@ -1,5 +1,3 @@ -"""Custom Accordion component.""" - from typing import Any, Literal from reflex.components.component import Component, ComponentNamespace @@ -32,8 +30,6 @@ class ClassNames: class AccordionBaseComponent(BaseUIComponent): - """Base component for accordion components.""" - library = f"{PACKAGE_NAME}/accordion" @property @@ -47,34 +43,24 @@ class AccordionRoot(AccordionBaseComponent): tag = "Accordion.Root" - # The uncontrolled value of the item(s) that should be initially expanded. To render a controlled accordion, use the `value` prop instead. default_value: Var[list[Any]] - # The controlled value of the item(s) that should be expanded. To render an uncontrolled accordion, use the `default_value` prop instead. value: Var[list[Any]] - # Event handler called when an accordion item is expanded or collapsed. Provides the new value as an argument. on_value_change: EventHandler[passthrough_event_spec(list[str])] - # Allows the browser's built-in page search to find and expand the panel contents. Overrides the `keep_mounted` prop and uses `hidden="until-found"` to hide the element without removing it from the DOM. Defaults to False. hidden_until_found: Var[bool] - # Whether multiple items can be open at the same time. Defaults to True. multiple: Var[bool] - # Whether the component should ignore user interaction. Defaults to False. disabled: Var[bool] - # Whether to loop keyboard focus back to the first item when the end of the list is reached while using the arrow keys. Defaults to True. loop_focus: Var[bool] - # The visual orientation of the accordion. Controls whether roving focus uses left/right or up/down arrow keys. Defaults to 'vertical'. orientation: Var[LiteralOrientation] - # Whether to keep the element in the DOM while the panel is closed. This prop is ignored when hidden_until_found is used. Defaults to False. keep_mounted: Var[bool] - # The render prop. render_: Var[Component] @classmethod @@ -86,20 +72,14 @@ def create(cls, *children, **props) -> BaseUIComponent: class AccordionItem(AccordionBaseComponent): - """Groups an accordion header with the corresponding panel.""" - tag = "Accordion.Item" - # The value that identifies this item. value: Var[str] - # Event handler called when the panel is opened or closed. on_open_change: EventHandler[passthrough_event_spec(bool)] - # Whether the component should ignore user interaction. Defaults to False. disabled: Var[bool] - # The render prop. render_: Var[Component] @classmethod @@ -111,11 +91,8 @@ def create(cls, *children, **props) -> BaseUIComponent: class AccordionHeader(AccordionBaseComponent): - """A heading that labels the corresponding panel.""" - tag = "Accordion.Header" - # The render prop. render_: Var[Component] @classmethod @@ -127,13 +104,13 @@ def create(cls, *children, **props) -> BaseUIComponent: class AccordionTrigger(AccordionBaseComponent): - """A button that opens and closes the corresponding panel.""" - tag = "Accordion.Trigger" title: Var[str] + native_button: Var[bool] - render_: Var[Component] = None + + render_: Var[Component] | None = None @classmethod def create(cls, *children, **props) -> BaseUIComponent: @@ -172,17 +149,12 @@ def create(cls, *children, **props) -> BaseUIComponent: class AccordionPanel(AccordionBaseComponent): - """A collapsible panel with the accordion item contents.""" - tag = "Accordion.Panel" - # Allows the browser's built-in page search to find and expand the panel contents. Overrides the `keep_mounted` prop and uses `hidden="until-found"` to hide the element without removing it from the DOM. Defaults to False. hidden_until_found: Var[bool] - # Whether to keep the element in the DOM while the panel is closed. This prop is ignored when `hidden_until_found` is used. Defaults to False. keep_mounted: Var[bool] - # The render prop. render_: Var[Component] @classmethod @@ -193,132 +165,13 @@ def create(cls, *children, **props) -> BaseUIComponent: return super().create(*children, **props) -class HighLevelAccordion(AccordionRoot): - """High level wrapper for the Accordion component.""" - - items: Var[ITEMS_TYPE] | ITEMS_TYPE - - _item_props = {"on_open_change", "disabled"} - _trigger_props = {"native_button"} - _panel_props = {"hidden_until_found", "keep_mounted"} - - @classmethod - def create( - cls, - items: Var[ITEMS_TYPE] | ITEMS_TYPE, - **props, - ) -> BaseUIComponent: - """Create a high level accordion component. - - Args: - items: List of dictionaries with 'trigger', 'content', and optional 'value' and 'disabled' keys. - **props: Additional properties to apply to the accordion component. - - Returns: - The accordion component with all necessary subcomponents. - """ - # Extract props for different parts - item_props = {k: props.pop(k) for k in cls._item_props & props.keys()} - trigger_props = {k: props.pop(k) for k in cls._trigger_props & props.keys()} - panel_props = {k: props.pop(k) for k in cls._panel_props & props.keys()} - - if isinstance(items, Var): - accordion_items = foreach( - items, - lambda item: cls._create_accordion_item_dynamic( - item, item_props, trigger_props, panel_props - ), - ) - return AccordionRoot.create(accordion_items, **props) - accordion_items = [ - cls._create_accordion_item( - item, index, item_props, trigger_props, panel_props - ) - for index, item in enumerate(items) - ] - return AccordionRoot.create(*accordion_items, **props) - - @classmethod - def _create_trigger_icon(cls) -> Component: - """Create the accordion trigger icon.""" - return icon( - "PlusSignIcon", - class_name=ClassNames.TRIGGER_ICON, - data_slot="accordion-trigger-icon", - ) - - @classmethod - def _create_accordion_item( - cls, - item: dict[str, str | Component], - index: int, - item_props: dict, - trigger_props: dict, - panel_props: dict, - ) -> BaseUIComponent: - """Create a single accordion item from a dictionary (for normal lists).""" - return AccordionItem.create( - AccordionHeader.create( - AccordionTrigger.create( - item.get("trigger"), - cls._create_trigger_icon(), - **trigger_props, - ), - ), - AccordionPanel.create( - Div.create( - item.get("content"), - class_name=ClassNames.PANEL_DIV, - data_slot="accordion-panel-div", - ), - **panel_props, - ), - value=item.get("value", f"item-{index + 1}"), - disabled=item.get("disabled", False), - **item_props, - ) - - @classmethod - def _create_accordion_item_dynamic( - cls, - item: ObjectVar[dict[str, str | Component]], - item_props: dict, - trigger_props: dict, - panel_props: dict, - ) -> BaseUIComponent: - """Create a single accordion item from a dictionary (for Var items).""" - return AccordionItem.create( - AccordionHeader.create( - AccordionTrigger.create( - item["trigger"], - cls._create_trigger_icon(), - **trigger_props, - ), - ), - AccordionPanel.create( - Div.create( - item["content"], - class_name=ClassNames.PANEL_DIV, - data_slot="accordion-panel-div", - ), - **panel_props, - ), - value=item.get("value", ""), - disabled=item.get("disabled", False).bool(), - **item_props, - ) - - class Accordion(ComponentNamespace): - """Namespace for Accordion components.""" - root = staticmethod(AccordionRoot.create) item = staticmethod(AccordionItem.create) header = staticmethod(AccordionHeader.create) trigger = staticmethod(AccordionTrigger.create) panel = staticmethod(AccordionPanel.create) class_names = ClassNames - __call__ = staticmethod(HighLevelAccordion.create) accordion = Accordion() diff --git a/components/ui/attachment.py b/components/ui/attachment.py index c9c22d8..3704269 100644 --- a/components/ui/attachment.py +++ b/components/ui/attachment.py @@ -1,5 +1,3 @@ -"""Attachment component — file and image attachment previews for chat UI.""" - from typing import Literal import reflex as rx @@ -24,7 +22,7 @@ class ClassNames: "data-[state=idle]:border-dashed" ) - SIZES: dict[str, str] = { + SIZES = { "default": ( "gap-2 text-sm " "has-data-[slot=attachment-content]:px-2.5 " @@ -45,7 +43,7 @@ class ClassNames: ), } - ORIENTATIONS: dict[str, str] = { + ORIENTATIONS = { "horizontal": "min-w-40 items-center", "vertical": "w-24 flex-col has-data-[slot=attachment-content]:w-30", } @@ -65,7 +63,7 @@ class ClassNames: "group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5" ) - MEDIA_VARIANTS: dict[str, str] = { + MEDIA_VARIANTS = { "icon": "", "image": ( "opacity-60 " @@ -117,13 +115,7 @@ def attachment_root( class_name: str = "", **props, ) -> rx.Component: - """ - Root attachment container. - orientation: horizontal (default) | vertical - size: default | sm | xs - state: idle | uploading | processing | error | done (default) - """ return rx.el.div( *children, data_slot="attachment", @@ -146,7 +138,7 @@ def attachment_media( class_name: str = "", **props, ) -> rx.Component: - """Icon or image preview slot.""" + return rx.el.div( *children, data_slot="attachment-media", @@ -161,7 +153,7 @@ def attachment_media( def attachment_content(*children, class_name: str = "", **props) -> rx.Component: - """Text content area — holds title and description.""" + return rx.el.div( *children, data_slot="attachment-content", @@ -171,7 +163,7 @@ def attachment_content(*children, class_name: str = "", **props) -> rx.Component def attachment_title(*children, class_name: str = "", **props) -> rx.Component: - """Filename or attachment title. Shimmers during uploading/processing.""" + return rx.el.span( *children, data_slot="attachment-title", @@ -181,7 +173,7 @@ def attachment_title(*children, class_name: str = "", **props) -> rx.Component: def attachment_description(*children, class_name: str = "", **props) -> rx.Component: - """File type, size, or other metadata.""" + return rx.el.span( *children, data_slot="attachment-description", @@ -191,10 +183,7 @@ def attachment_description(*children, class_name: str = "", **props) -> rx.Compo def attachment_actions(*children, class_name: str = "", **props) -> rx.Component: - """ - Row of action buttons. - In vertical orientation, absolutely positioned top-right. - """ + return rx.el.div( *children, data_slot="attachment-actions", @@ -204,9 +193,10 @@ def attachment_actions(*children, class_name: str = "", **props) -> rx.Component def attachment_action(*children, class_name: str = "", **props) -> rx.Component: - """Individual action button.""" props.setdefault("variant", "ghost") + props.setdefault("size", "icon-xs") + return button( *children, data_slot="attachment-action", @@ -231,10 +221,7 @@ def attachment_trigger( def attachment_group(*children, class_name: str = "", **props) -> rx.Component: - """ - Horizontal scrolling row of attachments. - Snaps to each attachment on scroll. - """ + return rx.el.div( *children, data_slot="attachment-group", @@ -244,8 +231,6 @@ def attachment_group(*children, class_name: str = "", **props) -> rx.Component: class Attachment(ComponentNamespace): - """Attachment namespace.""" - root = staticmethod(attachment_root) media = staticmethod(attachment_media) content = staticmethod(attachment_content) diff --git a/components/ui/autocomplete.py b/components/ui/autocomplete.py index 0c932f8..bd09043 100644 --- a/components/ui/autocomplete.py +++ b/components/ui/autocomplete.py @@ -1,5 +1,3 @@ -"""Custom autocomplete component with flat and grouped item support.""" - from typing import Any from reflex.components.component import Component @@ -396,23 +394,35 @@ class ClassNames: class BuridanAutocomplete(Component): tag = "BuridanAutocompleteRoot" + is_default = False items: Var[Any] + placeholder: Var[str] + root_class: Var[str] + input_class: Var[str] + popup_class: Var[str] + list_class: Var[str] + item_class: Var[str] + item_highlighted_class: Var[str] + empty_class: Var[str] + group_label_class: Var[str] + separator_class: Var[str] set_query_on_select: Var[bool] = True on_change_query: EventHandler[passthrough_event_spec(str)] + on_select_item: EventHandler[passthrough_event_spec(str)] def add_custom_code(self) -> list[str]: diff --git a/components/ui/avatar.py b/components/ui/avatar.py index 460bf5d..99460f2 100644 --- a/components/ui/avatar.py +++ b/components/ui/avatar.py @@ -1,126 +1,125 @@ -"""Custom avatar component.""" - from reflex.components.component import Component, ComponentNamespace from reflex.event import EventHandler, passthrough_event_spec from reflex.utils.imports import ImportVar from reflex.vars.base import Var +from reflex_components_core.el import Div, Span +from ..utils.twmerge import cn from .base_ui import PACKAGE_NAME, BaseUIComponent +from .component import CoreComponent class ClassNames: - """Class names for avatar components.""" - - ROOT = "shrink-0 inline-flex size-8 items-center justify-center overflow-hidden rounded-full align-middle text-base font-medium select-none" - IMAGE = "size-full object-cover shrink-0" - FALLBACK = "flex size-full items-center justify-center text-sm bg-muted" + ROOT = "group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=xl]:size-14 data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten" + IMAGE = "aspect-square size-full rounded-full object-cover" + FALLBACK = "flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs" + BADGE = "absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2 group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2" + GROUP = "group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background" + GROUP_COUNT = "relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3" class AvatarBaseComponent(BaseUIComponent): - """Base component for avatar components.""" - library = f"{PACKAGE_NAME}/avatar" @property def import_var(self): - """Return the import variable for the avatar component.""" return ImportVar(tag="Avatar", package_path="", install=False) class AvatarRoot(AvatarBaseComponent): - """Displays a user's profile picture, initials, or fallback icon.""" - tag = "Avatar.Root" - - # The component to render render_: Var[Component] @classmethod def create(cls, *children, **props) -> BaseUIComponent: - """Create the avatar root component.""" + custom_classes = props.pop("class_name", "") + size = props.pop("size", "default") props["data-slot"] = "avatar" - cls.set_class_name(ClassNames.ROOT, props) - return super().create(*children, **props) + props["data-size"] = size + + return super().create( + *children, class_name=cn(ClassNames.ROOT, custom_classes), **props + ) class AvatarImage(AvatarBaseComponent): - """The image to be displayed in the avatar.""" - tag = "Avatar.Image" - - # The image source URL src: Var[str] - - # Callback when loading status changes on_loading_status_change: EventHandler[passthrough_event_spec(str)] - - # The component to render render_: Var[Component] @classmethod def create(cls, *children, **props) -> BaseUIComponent: - """Create the avatar image component.""" + custom_classes = props.pop("class_name", "") props["data-slot"] = "avatar-image" - cls.set_class_name(ClassNames.IMAGE, props) - return super().create(*children, **props) + return super().create( + *children, class_name=cn(ClassNames.IMAGE, custom_classes), **props + ) -class AvatarFallback(AvatarBaseComponent): - """Rendered when the image fails to load or when no image is provided.""" +class AvatarFallback(AvatarBaseComponent): tag = "Avatar.Fallback" - - # How long to wait before showing the fallback. Specified in milliseconds delay: Var[int] - - # The component to render render_: Var[Component] @classmethod def create(cls, *children, **props) -> BaseUIComponent: - """Create the avatar fallback component.""" + custom_classes = props.pop("class_name", "") props["data-slot"] = "avatar-fallback" - cls.set_class_name(ClassNames.FALLBACK, props) - return super().create(*children, **props) + return super().create( + *children, + class_name=cn(ClassNames.FALLBACK, custom_classes), + **props, + ) -class HighLevelAvatar(AvatarRoot): - """High level wrapper for the Avatar component.""" - # The image source URL - src: Var[str] +class AvatarBadge(Span, CoreComponent): + @classmethod + def create(cls, *children, **props) -> Span: + custom_classes = props.pop("class_name", "") + props["data-slot"] = "avatar-badge" + + return super().create( + *children, class_name=cn(ClassNames.BADGE, custom_classes), **props + ) - # Image props - _image_props = {"src", "on_loading_status_change", "render_"} - # Fallback props - _fallback_props = {"delay"} +class AvatarGroup(Div, CoreComponent): + """The flex container wrapper for grouping multiple avatars.""" @classmethod - def create(cls, *children, **props) -> BaseUIComponent: - """Create the avatar component.""" - # Extract props for each subcomponent - image_props = {k: props.pop(k) for k in cls._image_props & props.keys()} - fallback_props = {k: props.pop(k) for k in cls._fallback_props & props.keys()} + def create(cls, *children, **props) -> Div: + custom_classes = props.pop("class_name", "") + props["data-slot"] = "avatar-group" + + return super().create( + *children, class_name=cn(ClassNames.GROUP, custom_classes), **props + ) - fallback_content = props.pop("fallback", "") - return AvatarRoot.create( - AvatarImage.create(**image_props), - AvatarFallback.create(fallback_content, **fallback_props), +class AvatarGroupCount(Div, CoreComponent): + @classmethod + def create(cls, *children, **props) -> Div: + custom_classes = props.pop("class_name", "") + props["data-slot"] = "avatar-group-count" + + return super().create( *children, + class_name=cn(ClassNames.GROUP_COUNT, custom_classes), **props, ) class Avatar(ComponentNamespace): - """Namespace for Avatar components.""" - root = staticmethod(AvatarRoot.create) image = staticmethod(AvatarImage.create) fallback = staticmethod(AvatarFallback.create) + badge = staticmethod(AvatarBadge.create) + group = staticmethod(AvatarGroup.create) + group_count = staticmethod(AvatarGroupCount.create) class_names = ClassNames - __call__ = staticmethod(HighLevelAvatar.create) avatar = Avatar() diff --git a/components/ui/badge.py b/components/ui/badge.py index 5e1a13c..5c5496f 100644 --- a/components/ui/badge.py +++ b/components/ui/badge.py @@ -3,86 +3,54 @@ from reflex.vars.base import Var from reflex_components_core.el import Span +from ..utils.twmerge import cn from .component import CoreComponent -LiteralBadgeVariant = Literal["default", "secondary", "destructive", "outline"] +LiteralBadgeVariant = Literal[ + "default", "secondary", "destructive", "outline", "ghost", "link" +] DEFAULT_BASE_CLASSES = ( - "inline-flex items-center justify-center rounded-radius border px-2 py-0.5 text-xs font-medium " - "w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none " - "focus-visible:border-[var(--ring)] focus-visible:ring-[var(--ring)]/50 focus-visible:ring-[3px] " - "aria-invalid:ring-[var(--destructive)]/20 dark:aria-invalid:ring-[var(--destructive)]/40 " - "aria-invalid:border-[var(--destructive)] transition-[color,box-shadow] overflow-hidden" + "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden " + "rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap " + "transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 " + "has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive " + "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!" ) BADGE_VARIANTS = { - "default": ( - "border-transparent bg-[var(--primary)] text-[var(--primary-foreground)] " - "[a&]:hover:bg-[var(--primary)]/90" - ), - "secondary": ( - "border-transparent bg-[var(--secondary)] text-[var(--secondary-foreground)] " - "[a&]:hover:bg-[var(--secondary)]/90" - ), + "default": "bg-primary text-primary-foreground [a]:hover:bg-primary/80", + "secondary": "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", "destructive": ( - "border-transparent bg-[var(--destructive)] text-white " - "[a&]:hover:bg-[var(--destructive)]/90 " - "focus-visible:ring-[var(--destructive)]/20 dark:focus-visible:ring-[var(--destructive)]/40 " - "dark:bg-[var(--destructive)]/60" - ), - "outline": ( - "text-[var(--foreground)] border-[var(--input)] " - "[a&]:hover:bg-[var(--accent)] [a&]:hover:text-[var(--accent-foreground)]" + "bg-destructive/10 text-destructive focus-visible:ring-destructive/20 " + "dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20" ), + "outline": "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", + "ghost": "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", + "link": "text-primary underline-offset-4 hover:underline", } -def get_badge_classes(variant: LiteralBadgeVariant) -> str: - """Get the complete badge class string. - - Args: - variant: The badge variant to apply - - Returns: - The complete class string for the badge - """ - variant_classes = BADGE_VARIANTS[variant] - return f"{DEFAULT_BASE_CLASSES} {variant_classes}" +def badge_variants(variant: str = "default") -> Var: + return cn( + DEFAULT_BASE_CLASSES, + BADGE_VARIANTS.get(variant, BADGE_VARIANTS["default"]), + ) class Badge(Span, CoreComponent): - """A badge component that displays a label.""" - - # Badge variant - variant: Var[LiteralBadgeVariant] - @classmethod def create(cls, *children, **props) -> Span: - """Create the badge component. - - Args: - *children: The badge content - **props: Component properties including variant - - Returns: - A configured Span component - """ + custom_classes = props.pop("class_name", "") variant = props.pop("variant", "default") - - cls.set_class_name(get_badge_classes(variant), props) - - # Add data-slot attribute - props.setdefault("data_slot", "badge") - - return super().create(*children, **props) - - def _exclude_props(self) -> list[str]: - """Exclude component-specific props from being passed to the DOM. - - Returns: - List of prop names to exclude - """ - return [*super()._exclude_props(), "variant"] + props["data-slot"] = "badge" + props["data-variant"] = variant + + return super().create( + *children, + class_name=cn(badge_variants(variant), custom_classes), + **props, + ) badge = Badge.create diff --git a/components/ui/breadcrumb.py b/components/ui/breadcrumb.py index 099a435..a4069b8 100644 --- a/components/ui/breadcrumb.py +++ b/components/ui/breadcrumb.py @@ -1,95 +1,135 @@ import reflex as rx +from ..icons.hugeicon import hi +from ..utils.twmerge import cn -def breadcrumb(*children, **props): - """Breadcrumb navigation container""" - return rx.el.nav( - *children, aria_label="breadcrumb", data_slot="breadcrumb", **props - ) - - -def breadcrumb_list(*children, class_name: str = "", **props): - """Ordered list container for breadcrumb items""" - base_classes = ( - "text-[var(--muted-foreground)] flex flex-wrap items-center gap-1 text-sm " - "break-words sm:gap-2.5" - ) - - return rx.el.ol( - *children, - data_slot="breadcrumb-list", - class_name=f"{base_classes} {class_name}".strip(), - **props, - ) - - -def breadcrumb_item(*children, class_name: str = "", **props): - """Individual breadcrumb item""" - base_classes = "inline-flex items-center gap-1.5" - - return rx.el.li( - *children, - data_slot="breadcrumb-item", - class_name=f"{base_classes} {class_name}".strip(), - **props, - ) - - -def breadcrumb_link(*children, href: str = "#", class_name: str = "", **props): - """Breadcrumb link (clickable)""" - base_classes = "hover:text-[var(--foreground)] transition-colors no-underline" - - return rx.el.a( - *children, - href=href, - data_slot="breadcrumb-link", - class_name=f"{base_classes} {class_name}".strip(), - **props, - ) - - -def breadcrumb_page(*children, class_name: str = "", **props): - """Current page breadcrumb (non-clickable)""" - base_classes = "text-[var(--foreground)] font-normal" - - return rx.el.span( - *children, - role="link", - aria_disabled="true", - aria_current="page", - data_slot="breadcrumb-page", - class_name=f"{base_classes} {class_name}".strip(), - **props, - ) - - -def breadcrumb_separator(*children, class_name: str = "", **props): - """Separator between breadcrumb items""" - base_classes = "[&>svg]:size-3.5" - - if not children: - children = (rx.icon(tag="chevron-right", size=14),) - - return rx.el.li( - *children, - role="presentation", - aria_hidden="true", - data_slot="breadcrumb-separator", - class_name=f"{base_classes} {class_name}".strip(), - **props, - ) - - -def breadcrumb_ellipsis(class_name: str = "", **props): - """Ellipsis for collapsed breadcrumb items""" - base_classes = "flex size-9 items-center justify-center" - - return rx.el.span( - rx.icon(tag="ellipsis", size=16), - rx.el.span("More", class_name="sr-only"), - role="presentation", - aria_hidden="true", - data_slot="breadcrumb-ellipsis", - class_name=f"{base_classes} {class_name}".strip(), - **props, - ) + +class Breadcrumb: + @classmethod + def root(cls, *children, **props) -> rx.Component: + custom_classes = props.pop("class_name", "") + + props["aria-label"] = "breadcrumb" + + props["data-slot"] = "breadcrumb" + + return rx.el.nav( + *children, + class_name=cn(custom_classes), + **props, + ) + + @classmethod + def list(cls, *children, **props) -> rx.Component: + custom_classes = props.pop("class_name", "") + + props["data-slot"] = "breadcrumb-list" + + base_classes = ( + "flex flex-wrap items-center gap-1.5 text-sm wrap-break-word " + "text-muted-foreground" + ) + + return rx.el.ol( + *children, + class_name=cn(base_classes, custom_classes), + **props, + ) + + @classmethod + def item(cls, *children, **props) -> rx.Component: + custom_classes = props.pop("class_name", "") + + props["data-slot"] = "breadcrumb-item" + + base_classes = "inline-flex items-center gap-1" + + return rx.el.li( + *children, + class_name=cn(base_classes, custom_classes), + **props, + ) + + @classmethod + def link(cls, *children, **props) -> rx.Component: + custom_classes = props.pop("class_name", "") + + props["data-slot"] = "breadcrumb-link" + + props.setdefault("href", "#") + + base_classes = "transition-colors hover:text-foreground no-underline" + + return rx.el.a( + *children, + class_name=cn(base_classes, custom_classes), + **props, + ) + + @classmethod + def page(cls, *children, **props) -> rx.Component: + custom_classes = props.pop("class_name", "") + + props["data-slot"] = "breadcrumb-page" + + props["role"] = "link" + + props["aria-disabled"] = "true" + + props["aria-current"] = "page" + + base_classes = "font-normal text-foreground" + + return rx.el.span( + *children, + class_name=cn(base_classes, custom_classes), + **props, + ) + + @classmethod + def separator(cls, *children, **props) -> rx.Component: + custom_classes = props.pop("class_name", "") + + props["data-slot"] = "breadcrumb-separator" + + props["role"] = "presentation" + + props["aria-hidden"] = "true" + + base_classes = "[&>svg]:size-3.5" + + if not children: + children = (hi("ArrowRight01Icon"),) + + return rx.el.li( + *children, + class_name=cn(base_classes, custom_classes), + **props, + ) + + @classmethod + def ellipsis(cls, *children, **props) -> rx.Component: + custom_classes = props.pop("class_name", "") + + props["data-slot"] = "breadcrumb-ellipsis" + + props["role"] = "presentation" + + props["aria-hidden"] = "true" + + base_classes = "flex size-5 items-center justify-center [&>svg]:size-4" + + if not children: + children = ( + rx.icon(tag="ellipsis"), + rx.el.span("More", class_name="sr-only"), + ) + + return rx.el.span( + *children, + class_name=cn(base_classes, custom_classes), + **props, + ) + + +breadcrumb = Breadcrumb diff --git a/components/ui/button.py b/components/ui/button.py index 9fa7a89..07b0f55 100644 --- a/components/ui/button.py +++ b/components/ui/button.py @@ -1,121 +1,139 @@ from typing import Literal from reflex.vars.base import Var -from reflex_components_core.core import cond from reflex_components_core.el import Button as BaseButton -from ..icons.others import spinner +from ..utils.twmerge import cn from .component import CoreComponent LiteralButtonVariant = Literal[ - "primary", "destructive", "outline", "secondary", "ghost", "link", "dark" + "default", + "destructive", + "outline", + "secondary", + "ghost", + "link", ] LiteralButtonSize = Literal[ - "default", "xs", "sm", "lg", "icon", "icon-xs", "icon-sm", "icon-lg" + "default", + "xs", + "sm", + "lg", + "icon", + "icon-xs", + "icon-sm", + "icon-lg", ] DEFAULT_CLASS_NAME = ( - "font-theme inline-flex items-center justify-center gap-2 whitespace-nowrap " - "rounded-radius text-sm font-medium transition-all " - "disabled:pointer-events-none disabled:opacity-50 outline-none " - "[&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 " - "[&_svg]:shrink-0 shrink-0" + "group/button inline-flex shrink-0 items-center justify-center " + "rounded-lg border border-transparent bg-clip-padding " + "text-sm font-medium whitespace-nowrap transition-all outline-none select-none " + "focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 " + "active:not-aria-[haspopup]:translate-y-px " + "disabled:pointer-events-none disabled:opacity-50 " + "aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 " + "dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 " + "[&_svg]:pointer-events-none [&_svg]:shrink-0 " + "[&_svg:not([class*='size-'])]:size-4" ) BUTTON_VARIANTS = { "variant": { - "default": "bg-primary text-primary-foreground hover:bg-primary/90", + "default": ("bg-primary text-primary-foreground hover:bg-primary/80"), + "outline": ( + "border-border bg-background hover:bg-muted hover:text-foreground " + "aria-expanded:bg-muted aria-expanded:text-foreground " + "dark:border-input dark:bg-input/30 dark:hover:bg-input/50" + ), + "secondary": ( + "bg-secondary text-secondary-foreground " + "hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] " + "aria-expanded:bg-secondary aria-expanded:text-secondary-foreground" + ), + "ghost": ( + "hover:bg-muted hover:text-foreground " + "aria-expanded:bg-muted aria-expanded:text-foreground " + "dark:hover:bg-muted/50" + ), "destructive": ( "bg-destructive/10 text-destructive hover:bg-destructive/20 " "focus-visible:border-destructive/40 focus-visible:ring-destructive/20 " - "dark:bg-destructive/20 dark:hover:bg-destructive/30" - ), - "outline": ( - "border border-input bg-background shadow-xs text-foreground " - "hover:bg-accent hover:text-accent-foreground " - "dark:bg-input/30 dark:hover:bg-input/50" + "dark:bg-destructive/20 dark:hover:bg-destructive/30 " + "dark:focus-visible:ring-destructive/40" ), - "secondary": "bg-secondary text-secondary-foreground hover:bg-secondary/80", - "ghost": "hover:bg-accent hover:text-accent-foreground dark:hover:bg-muted/50", "link": "text-primary underline-offset-4 hover:underline", }, "size": { - "default": "h-8 gap-1.5 px-2.5 has-[>svg]:px-2.5", - "xs": "h-6 rounded-[min(calc(var(--radius) * 0.8),10px)] gap-1 px-2 text-xs", - "sm": "h-7 rounded-[min(calc(var(--radius) * 0.8),12px)] gap-1 px-2.5 text-[0.8rem]", - "lg": "h-9 gap-1.5 px-2.5", + "default": ( + "h-8 gap-1.5 px-2.5 " + "has-data-[icon=inline-end]:pr-2 " + "has-data-[icon=inline-start]:pl-2" + ), + "xs": ( + "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs " + "in-data-[slot=button-group]:rounded-lg " + "has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 " + "[&_svg:not([class*='size-'])]:size-3" + ), + "sm": ( + "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] " + "in-data-[slot=button-group]:rounded-lg " + "has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 " + "[&_svg:not([class*='size-'])]:size-3.5" + ), + "lg": ( + "h-9 gap-1.5 px-2.5 " + "has-data-[icon=inline-end]:pr-2 " + "has-data-[icon=inline-start]:pl-2" + ), "icon": "size-8", - "icon-xs": "size-6 rounded-[min(calc(var(--radius) * 0.8),10px)]", - "icon-sm": "size-7 rounded-[min(calc(var(--radius) * 0.8),12px)]", + "icon-xs": ( + "size-6 rounded-[min(var(--radius-md),10px)] " + "in-data-[slot=button-group]:rounded-lg " + "[&_svg:not([class*='size-'])]:size-3" + ), + "icon-sm": ( + "size-7 rounded-[min(var(--radius-md),12px)] " + "in-data-[slot=button-group]:rounded-lg" + ), "icon-lg": "size-9", }, } class Button(BaseButton, CoreComponent): - """A custom button component.""" - - # Button variant. Defaults to "primary". variant: Var[LiteralButtonVariant] - - # Button size. Defaults to "md". size: Var[LiteralButtonSize] - # The loading state of the button - loading: Var[bool] - @classmethod def create(cls, *children, **props) -> BaseButton: - """Create the button component.""" variant = props.pop("variant", "default") - cls.validate_variant(variant) - size = props.pop("size", "default") - cls.validate_size(size) - - loading = props.pop("loading", False) - disabled = props.pop("disabled", False) - - button_classes = f"{DEFAULT_CLASS_NAME} {BUTTON_VARIANTS['variant'][variant]} {BUTTON_VARIANTS['size'][size]}" + custom_classes = props.pop("class_name", "") + + return super().create( + *children, + data_slot="button", + class_name=cn( + DEFAULT_CLASS_NAME, + BUTTON_VARIANTS["variant"].get(variant, ""), + BUTTON_VARIANTS["size"].get(size, ""), + custom_classes, + ), + **props, + ) - cls.set_class_name(button_classes, props) - - children_list = list(children) - - if isinstance(loading, Var): - props["disabled"] = cond(loading, True, disabled) - children_list.insert(0, cond(loading, spinner())) - else: - props["disabled"] = True if loading else disabled - children_list.insert(0, spinner()) if loading else None - - return super().create(*children_list, **props) - - @staticmethod - def validate_variant(variant: LiteralButtonVariant): - """Validate the button variant.""" - if variant not in BUTTON_VARIANTS["variant"]: - available_variants = ", ".join(BUTTON_VARIANTS["variant"].keys()) - message = ( - f"Invalid variant: {variant}. Available variants: {available_variants}" - ) - raise ValueError(message) + def _exclude_props(self) -> list[str]: + return [*super()._exclude_props(), "size", "variant"] - @staticmethod - def validate_size(size: LiteralButtonSize): - """Validate the button size.""" - if size not in BUTTON_VARIANTS["size"]: - available_sizes = ", ".join(BUTTON_VARIANTS["size"].keys()) - message = f"Invalid size: {size}. Available sizes: {available_sizes}" - raise ValueError(message) - def _exclude_props(self) -> list[str]: - return [ - *super()._exclude_props(), - "size", - "variant", - "loading", - ] +def button_variants(variant: str = "default", size: str = "default") -> Var: + return cn( + DEFAULT_CLASS_NAME, + BUTTON_VARIANTS["variant"].get(variant, ""), + BUTTON_VARIANTS["size"].get(size, ""), + ) button = Button.create diff --git a/components/ui/component.py b/components/ui/component.py index 5fba2bc..2592910 100644 --- a/components/ui/component.py +++ b/components/ui/component.py @@ -11,7 +11,6 @@ class CoreComponent(Component): """Core component for all components.""" - # Whether the component should be unstyled unstyled: Var[bool] @classmethod diff --git a/components/ui/input.py b/components/ui/input.py index c0ac43a..b3a543e 100644 --- a/components/ui/input.py +++ b/components/ui/input.py @@ -5,7 +5,7 @@ "w-full file:text-foreground placeholder:text-muted-foreground " "selection:bg-primary selection:text-primary-foreground " "dark:bg-input/30 border-input " - "h-9 w-full min-w-0 rounded-radius border bg-transparent px-3 py-1 text-base " + "h-9 w-full min-w-0 rounded-lg border bg-transparent px-3 py-1 text-base " "transition-[color,box-shadow] outline-none " "file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium " "disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 " diff --git a/components/ui/menu.py b/components/ui/menu.py index 364ad3e..3138bbc 100644 --- a/components/ui/menu.py +++ b/components/ui/menu.py @@ -1,18 +1,12 @@ -"""Custom menu component.""" - from typing import Literal from reflex.components.component import Component, ComponentNamespace from reflex.event import EventHandler, passthrough_event_spec from reflex.utils.imports import ImportVar from reflex.vars.base import Var -from reflex_components_core.core.foreach import foreach from ..icons.hugeicon import hi -from ..icons.others import select_arrow -from ..utils.twmerge import cn from .base_ui import PACKAGE_NAME, BaseUIComponent -from .button import button LiteralOpenChangeReason = Literal[ "arrowKey", @@ -40,7 +34,7 @@ class ClassNames: POPUP = ( "z-50 max-h-[var(--available-height)] w-[var(--anchor-width)] min-w-32 " "origin-[var(--transform-origin)] overflow-x-hidden overflow-y-auto " - "rounded-radius bg-popover p-1 text-popover-foreground shadow-md " + "rounded-lg bg-popover p-1 text-popover-foreground shadow-md " "ring-1 ring-foreground/10 duration-100 outline-none " "data-[side=bottom]:slide-in-from-top-2 " "data-[side=left]:slide-in-from-right-2 " @@ -51,7 +45,7 @@ class ClassNames: ) ITEM = ( "group/menu-item relative flex cursor-default items-center gap-1.5 " - "rounded-radius px-1.5 py-1 text-sm outline-hidden select-none " + "rounded-lg px-1.5 py-1 text-sm outline-hidden select-none " "focus:bg-accent focus:text-accent-foreground " "not-data-[variant=destructive]:focus:**:text-accent-foreground " "data-inset:pl-7 " @@ -67,7 +61,7 @@ class ClassNames: POSITIONER_SUB = "isolate z-50 outline-none" POPUP_SUB = ( "z-50 w-auto min-w-24 origin-(--transform-origin) overflow-x-hidden " - "overflow-y-auto rounded-radius bg-popover p-1 text-popover-foreground " + "overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground " "shadow-lg ring-1 ring-foreground/10 duration-100 " "data-[side=bottom]:slide-in-from-top-2 " "data-[side=left]:slide-in-from-right-2 " @@ -80,7 +74,7 @@ class ClassNames: "px-1.5 py-1 text-xs font-medium text-muted-foreground data-inset:pl-7" ) SUBMENU_TRIGGER = ( - "flex cursor-default items-center justify-between gap-1.5 rounded-radius px-1.5 py-1 " + "flex cursor-default items-center justify-between gap-1.5 rounded-lg px-1.5 py-1 " "text-sm outline-hidden select-none " "focus:bg-accent focus:text-accent-foreground " "not-data-[variant=destructive]:focus:**:text-accent-foreground " @@ -296,18 +290,6 @@ def create(cls, *children, **props) -> BaseUIComponent: return super().create( *children, **props, - # style={ - # "keyframes": { - # "animate-in": { - # "from": {"opacity": "0", "transform": "scale(0.95)"}, - # "to": {"opacity": "1", "transform": "scale(1)"}, - # }, - # "animate-out": { - # "from": {"opacity": "1", "transform": "scale(2)"}, - # "to": {"opacity": "0", "transform": "scale(0.95)"}, - # }, - # }, - # }, ) @@ -618,133 +600,6 @@ def create(cls, *children, **props) -> BaseUIComponent: return super().create(*children, **props) -class HighLevelMenu(MenuRoot): - """High level wrapper for the Menu component.""" - - # The trigger component to use for the menu - trigger: Var[Component | None] - - # The list of items to display in the menu dropdown - can be strings or tuples of (label, on_click_handler) - items: Var[list[str | tuple[str, EventHandler]]] - - # The placeholder text to display when no item is selected - placeholder: Var[str] - - # The size of the menu. Defaults to "md". - size: Var[LiteralMenuSize] - - # Whether to close the menu when the item is clicked. Defaults to True. - close_on_click: Var[bool] - - # Props for different component parts - _item_props = {"close_on_click"} - _trigger_props = {"placeholder", "size", "close_on_click"} - _positioner_props = { - "align", - "align_offset", - "side", - "arrow_padding", - "collision_padding", - "sticky", - "position_method", - "track_anchor", - "side_offset", - "collision_avoidance", - } - _portal_props = {"container"} - - @classmethod - def create(cls, *children, **props) -> BaseUIComponent: - """Create a menu component. - - Args: - *children: Additional children to include in the menu. - **props: Additional properties to apply to the menu component. - - Returns: - The menu component. - """ - # Extract props for different parts - item_props = {k: props.pop(k) for k in cls._item_props & props.keys()} - trigger_props = {k: props.pop(k) for k in cls._trigger_props & props.keys()} - positioner_props = { - k: props.pop(k) for k in cls._positioner_props & props.keys() - } - portal_props = {k: props.pop(k) for k in cls._portal_props & props.keys()} - - trigger = props.pop("trigger", None) - items = props.pop("items", []) - size = trigger_props.get("size", "md") - trigger_label = trigger_props.get("placeholder", "Open Menu") - - def create_menu_item(item: str | tuple[str, EventHandler]) -> BaseUIComponent: - if isinstance(item, tuple): - label, on_click_handler = item - return MenuItem.create( - render_=button( - label, - variant="ghost", - class_name=ClassNames.ITEM, - disabled=props.get("disabled", False), - on_click=on_click_handler, - size=size, - type="button", - ), - key=label, - **item_props, - ) - return MenuItem.create( - render_=button( - item, - variant="ghost", - class_name=ClassNames.ITEM, - disabled=props.get("disabled", False), - size=size, - type="button", - ), - key=item, - **item_props, - ) - - if isinstance(items, Var): - items_children = foreach(items, create_menu_item) - else: - items_children = [create_menu_item(item) for item in items] - - return MenuRoot.create( - MenuTrigger.create( - render_=( - trigger - if trigger - else button( - trigger_label, - select_arrow(class_name="size-4 text-secondary-9"), - variant="outline", - class_name=ClassNames.TRIGGER, - disabled=props.get("disabled", False), - size=size, - type="button", - ) - ), - ), - MenuPortal.create( - MenuPositioner.create( - MenuPopup.create( - items_children, - class_name=cn( - ClassNames.POPUP, - "", - ), - ), - **positioner_props, - ), - **portal_props, - ), - *children, - **props, - ) - - class Menu(ComponentNamespace): """Namespace for Menu components.""" @@ -766,7 +621,6 @@ class Menu(ComponentNamespace): submenu_root = staticmethod(MenuSubMenuRoot.create) submenu_trigger = staticmethod(MenuSubMenuTrigger.create) class_names = ClassNames - __call__ = staticmethod(HighLevelMenu.create) menu = Menu() diff --git a/components/ui/metric.py b/components/ui/metric.py index 8c31625..d8e0137 100644 --- a/components/ui/metric.py +++ b/components/ui/metric.py @@ -16,9 +16,7 @@ class ClassNames: """Class names for metric components.""" - ROOT = ( - "flex flex-col gap-y-2 p-4 rounded-radius border border-input bg-card shadow-sm" - ) + ROOT = "flex flex-col gap-y-2 p-4 rounded-lg border border-input bg-card shadow-sm" LABEL = "text-sm font-medium text-muted-foreground" VALUE = "text-3xl font-bold tracking-tight text-foreground" TREND_CONTAINER = "flex flex-row items-center gap-x-1.5" diff --git a/components/ui/select.py b/components/ui/select.py index dac0d2a..044a963 100644 --- a/components/ui/select.py +++ b/components/ui/select.py @@ -24,7 +24,7 @@ class ClassNames: """Class names for select components.""" - TRIGGER = "flex w-fit items-center justify-between gap-1.5 rounded-radius border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" + TRIGGER = "flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" VALUE = "flex-1 text-left cursor-default" ICON = "flex size-4 text-secondary-10 group-data-[disabled]/trigger:text-current" POPUP = "relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95" @@ -573,7 +573,7 @@ def create(cls, *children, **props) -> BaseUIComponent: class_name=cn( ClassNames.POPUP, "", - "rounded-radius", + "rounded-lg", ), ), **positioner_props, diff --git a/components/ui/slider.py b/components/ui/slider.py index f639ec9..55a618a 100644 --- a/components/ui/slider.py +++ b/components/ui/slider.py @@ -38,19 +38,19 @@ class ClassNames: ) TRACK = ( - "h-1 w-full rounded-radius bg-secondary select-none " + "h-1 w-full rounded-lg bg-secondary select-none " "data-[orientation=vertical]:h-full " "data-[orientation=vertical]:w-1" ) INDICATOR = ( - "absolute h-full rounded-radius bg-primary select-none " + "absolute h-full rounded-lg bg-primary select-none " "data-[orientation=vertical]:w-full " "data-[orientation=vertical]:h-auto" ) THUMB = ( - "size-3 rounded-radius bg-white outline-[1px] outline-black " + "size-3 rounded-lg bg-white outline-[1px] outline-black " "select-none box-shadow:[0_0_0_1px_rgba(0,0,0,1),0_1px_2px_rgba(0,0,0,.04)] " "data-[dragging]:h-5 transition-[height,scale] hover:h-4.5" ) diff --git a/components/ui/spinner.py b/components/ui/spinner.py index bc320de..a1ef560 100644 --- a/components/ui/spinner.py +++ b/components/ui/spinner.py @@ -7,6 +7,12 @@ def spinner(class_name: str = "", **props) -> rx.Component: + incoming_attrs = props.pop("custom_attrs", {}) + + base_attrs = {"viewBox": "0 0 16 16"} + + merged_attrs = {**base_attrs, **incoming_attrs} + return svg( svg.path( opacity="0.2", @@ -20,7 +26,7 @@ def spinner(class_name: str = "", **props) -> rx.Component: stroke_width="1.5", ), xmlns="http://www.w3.org/2000/svg", - custom_attrs={"viewBox": "0 0 16 16"}, + custom_attrs=merged_attrs, class_name=cn("size-4 animate-spin fill-none", class_name), data_slot="spinner", role="status", diff --git a/components/ui/switch.py b/components/ui/switch.py index 74bc014..3881e1a 100644 --- a/components/ui/switch.py +++ b/components/ui/switch.py @@ -11,7 +11,7 @@ class ClassNames: """Class names for switch components.""" - ROOT = "relative flex h-5 w-8 rounded-full bg-secondary p-0.5 transition-colors duration-200 ease-out before:absolute before:rounded-radius before:outline-offset-2 before:outline-primary focus-visible:before:inset-0 data-[checked]:bg-primary disabled:opacity-50 disabled:cursor-not-allowed cursor-default" + ROOT = "relative flex h-5 w-8 rounded-full bg-secondary p-0.5 transition-colors duration-200 ease-out before:absolute before:rounded-lg before:outline-offset-2 before:outline-primary focus-visible:before:inset-0 data-[checked]:bg-primary disabled:opacity-50 disabled:cursor-not-allowed cursor-default" THUMB = "aspect-square h-full rounded-full bg-white transition-transform duration-200 ease-out data-[checked]:translate-x-3 shadow-small" diff --git a/components/ui/table.py b/components/ui/table.py index 43cf9bf..8f90453 100644 --- a/components/ui/table.py +++ b/components/ui/table.py @@ -26,7 +26,7 @@ class ClassNames: """Class names for table components.""" - ROOT = "w-full overflow-auto rounded-radius border border-input bg-card shadow-sm" + ROOT = "w-full overflow-auto rounded-lg border border-input bg-card shadow-sm" TABLE = "w-full caption-bottom text-sm border-collapse" HEADER = "[&_tr]:border-b bg-secondary/50 backdrop-blur-sm sticky top-0" BODY = "[&_tr:last-child]:border-0" diff --git a/components/ui/tabs.py b/components/ui/tabs.py index bc2a3d9..0efb1f0 100644 --- a/components/ui/tabs.py +++ b/components/ui/tabs.py @@ -17,7 +17,7 @@ class ClassNames: LIST = ( "relative bg-muted text-muted-foreground inline-flex h-9 w-full w-fit items-center " - "justify-center rounded-radius p-[3px] " + "justify-center rounded-lg p-[3px] " "data-[orientation=vertical]:flex data-[orientation=vertical]:flex-col " "data-[orientation=vertical]:h-auto " "data-[orientation=vertical]:p-1 data-[orientation=vertical]:gap-1" @@ -26,7 +26,7 @@ class ClassNames: "relative z-[1] flex items-center justify-center text-sm font-medium " "text-foreground dark:text-muted-foreground " "data-[selected]:text-foreground " - "rounded-radius px-2 py-1 transition-all " + "rounded-lg px-2 py-1 transition-all " "data-[orientation=vertical]:justify-start data-[orientation=vertical]:w-full " "data-[orientation=vertical]:text-left" ) @@ -36,7 +36,7 @@ class ClassNames: ) INDICATOR = ( - "absolute z-[0] rounded-radius bg-background shadow-sm dark:border dark:border-input dark:bg-input/30 " + "absolute z-[0] rounded-lg bg-background shadow-sm dark:border dark:border-input dark:bg-input/30 " "transition-all duration-200 ease-in-out " "[left:var(--active-tab-left)] [top:var(--active-tab-top)] " "[width:var(--active-tab-width)] [height:var(--active-tab-height)]" diff --git a/components/ui/textarea.py b/components/ui/textarea.py index 428780b..93a1d83 100644 --- a/components/ui/textarea.py +++ b/components/ui/textarea.py @@ -9,7 +9,7 @@ class ClassNames: """Class names for textarea components.""" - ROOT = "flex field-sizing-content min-h-16 min-w-xs rounded-radius border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40" + ROOT = "flex field-sizing-content min-h-16 min-w-xs rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40" class Textarea(TextareaComponent, CoreComponent): diff --git a/components/ui/toggle.py b/components/ui/toggle.py index cf1825c..965cf7f 100644 --- a/components/ui/toggle.py +++ b/components/ui/toggle.py @@ -12,7 +12,7 @@ class ClassNames: """Class names for toggle components.""" - ROOT = "group/toggle p-1 inline-flex items-center justify-center gap-1 rounded-radius border border-input text-sm font-medium whitespace-nowrap transition-all hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" + ROOT = "group/toggle p-1 inline-flex items-center justify-center gap-1 rounded-lg border border-input text-sm font-medium whitespace-nowrap transition-all hover:bg-muted hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-pressed:bg-muted data-[state=on]:bg-muted dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" ICON_VARIANT_CLASSES = { "fill": "data-[pressed]:[&_svg]:fill-primary", diff --git a/components/ui/tooltip.py b/components/ui/tooltip.py index b5e40df..27e9702 100644 --- a/components/ui/tooltip.py +++ b/components/ui/tooltip.py @@ -21,7 +21,7 @@ class ClassNames: """Class names for tooltip components.""" TRIGGER = "inline-flex items-center justify-center" - POPUP = "z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-radius bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95" + POPUP = "z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-lg bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95" ARROW = "data-[side=bottom]:top-[-7.5px] data-[side=left]:right-[-12.5px] data-[side=left]:rotate-90 data-[side=right]:left-[-12.5px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-7.5px] data-[side=top]:rotate-180" diff --git a/docs/components/avatar.md b/docs/components/avatar.md index 048910c..f9a3796 100644 --- a/docs/components/avatar.md +++ b/docs/components/avatar.md @@ -23,24 +23,111 @@ Use the following composition to build an `Avatar` component. --ANATOMY(avatar)-- - # Examples +## Basic + +A basic avatar component with an image and a fallback. + +--DEMO(avatar_basic)-- + +## Badge + +Use the `avatar.badge` component to add a badge to the avatar. The badge is positioned at the bottom right of the avatar. + +--DEMO(avatar_with_badge)-- + +Use the `class_Name` prop to add custom styles to the badge such as custom colors, sizes, etc. + +```reflex +avatar.badge(class_name="bg-green-600 dark:bg-green-800") +``` + +## Badge with Icon -## General +You can also use an icon inside `avatar.badge`. -Displays a basic avatar with either a user image or a fallback placeholder. +--DEMO(avatar_badge_icon)-- ---DEMO(avatar_general)-- +## Avatar Group + +Use the `avatar.group` component to add a group of avatars. + +--DEMO(avatar_as_group)-- + +## Avatar Group Count + +Use `avatar.group_count` to add a count to the group. + +--DEMO(avatar_with_group_count)-- + +## Avatar Group with Icon + +You can also use an icon inside `avatar.group_count`. + +--DEMO(avatar_group_count_icon)-- ## Sizes -Demonstrates how to scale the avatar component using Tailwind utility classes. +Use the `size` prop to change the size of the avatar. --DEMO(avatar_sizes)-- -## With Badge +## Dropdown -Shows how to combine an avatar with status or notification badges for added context. +You can use the `Avatar` component as a trigger for a dropdown menu. ---DEMO(avatar_with_badge)-- +--DEMO(avatar_dropdown_menu)-- + +# API Reference + +## avatar.root + +The `avatar.root` component is the root component that wraps the avatar image and fallback. + +| Prop | Type | Default | +| ----------- | --------------------------- | ----------- | +| `size` | `"default" \| "sm" \| "lg"` | `"default"` | +| `class_name` | `string` | - | + +## avatar.image + +The `avatar.image` component displays the avatar image. It accepts all Base UI Avatar Image props. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `src` | `string` | - | +| `alt` | `string` | - | +| `class_name` | `string` | - | + +## avatar.fallback + +The `avatar.fallback` component displays a fallback when the image fails to load. It accepts all Base UI Avatar Fallback props. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | + +## avatar.badge + +The `avatar.badge` component displays a badge indicator on the avatar, typically positioned at the bottom right. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | + +## avatar.group + +The `avatar.group` component displays a group of avatars with overlapping styling. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | + +## avatar.group_count + +The `avatar.group_count` component displays a count indicator in an avatar group, typically showing the number of additional avatars. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | diff --git a/docs/components/badge.md b/docs/components/badge.md index 3bf64fb..6d843e1 100644 --- a/docs/components/badge.md +++ b/docs/components/badge.md @@ -23,30 +23,46 @@ Use the following composition to build a `Badge` component. --ANATOMY(badge)-- - # Examples +## Variants -## Default - -Displays a standard badge using the default variant, ideal for basic labeling. +Use the `variant` prop to change the variant of the badge. ---DEMO(badge_default)-- +--DEMO(badge_with_variants)-- ## With Icons -Demonstrates how to include icons inside badges for visual context or emphasis. +You can render an icon inside the badge. Use `data-icon="inline-start"` to render the icon on the left and `data-icon="inline-end"` to render the icon on the right. + +--DEMO(badge_with_icon)-- + +## With Spinner + +You can render a spinner inside the badge. Remember to add the `data-icon="inline-start"` or `data-icon="inline-end"` prop to the spinner. + +--DEMO(badge_with_spinner)-- + +## Link + +You can pass in `rx.el.a` to turn a badge into a link. The `badge` component accepts `*children` so any interactive element can be passed to it. + +--DEMO(badge_as_link)-- + +## Custom Colors ---DEMO(badge_with_icons)-- +You can customize the colors of a badge by adding custom classes such as `bg-green-50 dark:bg-green-800` to the `badge` component. -## Status +--DEMO(badge_custom_colors)-- -Showcases how badges can represent different statuses, like success or error, using color. ---DEMO(badge_status)-- +# API Reference -## Notification Count +## badge -Illustrates how to use badges for showing counts, such as unread notifications or messages. +The `badge` component displays a badge or a component that looks like a badge. ---DEMO(badge_notification_count)-- +| Prop | Type | Default | +| ----------- | ----------------------------------------------------------------------------- | ----------- | +| `variant` | `"default" \| "secondary" \| "destructive" \| "outline" \| "ghost" \| "link"` | `"default"` | +| `class_name` | `string` | - | diff --git a/docs/components/breadcrumb.md b/docs/components/breadcrumb.md index 0ceb9f7..68d14f7 100644 --- a/docs/components/breadcrumb.md +++ b/docs/components/breadcrumb.md @@ -22,22 +22,83 @@ Use the following composition to build a `Breadcrumb` component. # Examples -## Basic Demo -A basic breadcrumb showing the default navigation structure. +## Basic +A basic breadcrumb with a home link and a components link. ---DEMO(breadcrumb_basic_demo)-- +--DEMO(breadcrumb_basic)-- -## Simple Breadcrumb -A minimal breadcrumb with plain text links. +## Custom Separator +Use a custom component as `children` for `breadcrumb.separator` to create a custom separator. ---DEMO(breadcrumb_simple_breadcrumb)-- +--DEMO(breadcrumb_custom_separator)-- -## Icon Breadcrumb -A breadcrumb that includes icons alongside link labels. +## Dropdown ---DEMO(breadcrumb_icon_breadcrumb)-- +You can compose `breadcrumb.item` with a `menu.root` to create a dropdown in the breadcrumb. -## Custom Separator -A breadcrumb with a customized separator between items. +--DEMO(breadcrumb_dropdown_demo)-- ---DEMO(breadcrumb_custom_separator)-- +## Collapsed + +We provide a `breadcrumb.ellipsis` component to show a collapsed state when the breadcrumb is too long. + +--DEMO(breadcrumb_ellipsis)-- + +# API Reference + +## breadcrumb.root + +The `breadcrumb.root` component is the root navigation element that wraps all breadcrumb components. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | + +## breadcrumb.list + +The `breadcrumb.list` component displays the ordered list of breadcrumb items. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | + +## breadcrumb.item + +The `breadcrumb.item` component wraps individual breadcrumb items. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | + +## breadcrumb.link + +The `breadcrumb.link` component displays a clickable link in the breadcrumb. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | + +## breadcrumb.page + +The `breadcrumb.page` component displays the current page in the breadcrumb (non-clickable). + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | + +## breadcrumb.separator + +The `breadcrumb.separator` component displays a separator between breadcrumb items. You can pass custom children to override the default separator icon. + +| Prop | Type | Default | +| ----------- | ----------------- | ------- | +| `children` | `rx.Component` | - | +| `class_name` | `string` | - | + +## breadcrumb.ellipsis + +The `breadcrumb.ellipsis` component displays an ellipsis indicator for collapsed breadcrumb items. + +| Prop | Type | Default | +| ----------- | -------- | ------- | +| `class_name` | `string` | - | diff --git a/docs/components/button.md b/docs/components/button.md index b3eda77..b089839 100644 --- a/docs/components/button.md +++ b/docs/components/button.md @@ -23,54 +23,76 @@ Use the following composition to build a `Button` component. --ANATOMY(button)-- - # Examples - ## Sizes -Showcases buttons in different predefined sizes (default, small, large, icon, etc). +Use the `size` prop to change the size of the button. ---DEMO(button_sizes)-- +--DEMO(button_size)-- ## Default -The default visual style for buttons with standard background and hover effects. - --DEMO(button_default)-- ## Secondary -A more muted alternative to the default button, useful for less prominent actions. - --DEMO(button_secondary)-- ## Outline -Buttons with a bordered outline, blending well with minimal UIs or light themes. - --DEMO(button_outline)-- ## Ghost -A button style with no background or border, ideal for subtle UI actions. - --DEMO(button_ghost)-- ## Link -A button styled to look like a hyperlink — useful for inline actions or navigation. - --DEMO(button_link)-- ## Destructive -A bold style used for destructive or dangerous actions like “Delete”. - --DEMO(button_destructive)-- ## Icon -Examples showing icon-only buttons with varying sizes for compact UI elements. - --DEMO(button_icon)-- + +## With Icon + +Remember to add the `data-icon="inline-start"` or `data-icon="inline-end"` attribute to the icon for the correct spacing. + +--DEMO(button_with_icon)-- + +## Rounded + +Use the `rounded-full` class to make the button rounded. + +--DEMO(button_rounded)-- + +## Spinner + +Render a `spinner()` component inside the button to show a loading state. Remember to add the `data-icon="inline-start"` or `data-icon="inline-end"` attribute to the spinner for the correct spacing. + +--DEMO(button_loading)-- + +## As Link + +You can use the `button_variants` helper function to make a link look like a button. + +Do **not** use `button(rx.el.a(...))` for links. The Base UI `Button` component always applies `role="button"`, which overrides the semantic link role on `` elements. Use `button_variants` with a plain `rx.el.a` tag instead to cleanly generate the necessary classes as a dynamic Reflex `Var`. + +--DEMO(button_render)-- + + +# API Reference + +## Button + +The `Button` component is a wrapper around the `button` element that adds a variety of styles and functionality. + +| Prop | Type | Default | +| --------- | ------------------------------------------------------------------------------------ | ----------- | +| `variant` | `"default" \| "outline" \| "ghost" \| "destructive" \| "secondary" \| "link"` | `"default"` | +| `size` | `"default" \| "xs" \| "sm" \| "lg" \| "icon" \| "icon-xs" \| "icon-sm" \| "icon-lg"` | `"default"` | diff --git a/docs/resources/theming.md b/docs/resources/theming.md index a899c2e..f669564 100644 --- a/docs/resources/theming.md +++ b/docs/resources/theming.md @@ -73,7 +73,7 @@ Make sure to pull your CSS files where your tokens are defined in your `rx.App` app = rx.App(stylesheets=["globals.css"]) ``` -Tailwind maps these tokens into utilities like `bg-background`, `text-foreground`, `border-border`, and `rounded-radius`. Dark mode works by overriding the same tokens inside a `.dark` selector. +Tailwind maps these tokens into utilities like `bg-background`, `text-foreground`, `border-border`, and `rounded-lg`. Dark mode works by overriding the same tokens inside a `.dark` selector. # Theme Tokens diff --git a/rxconfig.py b/rxconfig.py index 310a90c..2d96522 100644 --- a/rxconfig.py +++ b/rxconfig.py @@ -55,7 +55,13 @@ "theme": "var(--font-family)", }, "borderRadius": { - "radius": "var(--radius)", + "sm": "var(--radius-sm)", + "md": "var(--radius-md)", + "lg": "var(--radius-lg)", + "xl": "var(--radius-xl)", + "2xl": "var(--radius-2xl)", + "3xl": "var(--radius-3xl)", + "4xl": "var(--radius-4xl)", }, "padding": { "card": "var(--card-padding)", diff --git a/uv.lock b/uv.lock index 5122bc1..1e88538 100644 --- a/uv.lock +++ b/uv.lock @@ -47,7 +47,7 @@ wheels = [ [[package]] name = "buridan-create" -version = "0.1.17" +version = "0.1.19" source = { editable = "." } [package.dev-dependencies]