diff --git a/docs/app/news/+icon-gallery.bugfix.md b/docs/app/news/+icon-gallery.bugfix.md new file mode 100644 index 00000000000..0ae657735d7 --- /dev/null +++ b/docs/app/news/+icon-gallery.bugfix.md @@ -0,0 +1 @@ +Fix the five `grid_2x_2`/`grid_3x_3` icons rendering blank in the icon gallery, and lighten the page by replacing per-icon tooltip popovers with native titles. diff --git a/docs/library/data-display/icon.md b/docs/library/data-display/icon.md index e176145b306..42fd21e5c51 100644 --- a/docs/library/data-display/icon.md +++ b/docs/library/data-display/icon.md @@ -37,17 +37,15 @@ def lucide_icons() -> rx.Component: LUCIDE_ICON_LIST, lambda icon: rx.cond( icon.startswith(icon_search_cs.value), - rx.tooltip( - rx.box( - rx.icon(tag=icon), - class_name="flex items-center justify-center rounded-md hover:bg-accent transition-bg p-2 cursor-pointer", - on_click=[ - rx.set_clipboard(icon), - rx.toast.success(f"Copied {icon} to clipboard"), - ], - key=icon, - ), - content=icon, + rx.box( + rx.icon(tag=icon), + title=icon, + class_name="flex items-center justify-center rounded-md hover:bg-accent transition-bg p-2 cursor-pointer", + on_click=[ + rx.set_clipboard(icon), + rx.toast.success(f"Copied {icon} to clipboard"), + ], + key=icon, ), None, ), diff --git a/packages/reflex-components-lucide/news/+dynamic-icon-digit-names.bugfix.md b/packages/reflex-components-lucide/news/+dynamic-icon-digit-names.bugfix.md new file mode 100644 index 00000000000..65705548764 --- /dev/null +++ b/packages/reflex-components-lucide/news/+dynamic-icon-digit-names.bugfix.md @@ -0,0 +1 @@ +Fix `rx.icon` with a Var tag rendering blank for digit-x-digit icons such as `grid_2x_2`, `grid_2x_2_check` and `grid_3x_3`. diff --git a/packages/reflex-components-lucide/src/reflex_components_lucide/icon.py b/packages/reflex-components-lucide/src/reflex_components_lucide/icon.py index 71cd3b8831f..c927c9b65cb 100644 --- a/packages/reflex-components-lucide/src/reflex_components_lucide/icon.py +++ b/packages/reflex-components-lucide/src/reflex_components_lucide/icon.py @@ -6,7 +6,7 @@ from reflex_base.components.component import Component, field from reflex_base.utils import format from reflex_base.utils.imports import ImportVar -from reflex_base.vars.base import LiteralVar, Var +from reflex_base.vars.base import LiteralVar, Var, var_operation, var_operation_return from reflex_base.vars.sequence import LiteralStringVar, StringVar logger = logging.getLogger(__name__) @@ -20,6 +20,26 @@ class LucideIconComponent(Component): library = LUCIDE_LIBRARY +@var_operation +def dynamic_icon_name_operation(tag: StringVar): + """Convert an icon tag to the name Lucide's DynamicIcon expects. + + Underscores become hyphens, and the hyphen that snake_case puts inside + digit-x-digit names (``grid_2x_2`` -> ``grid-2x-2``) is dropped so the + name matches Lucide's own ``grid-2x2``. + + Args: + tag: The snake_case or kebab-case icon tag. + + Returns: + The kebab-case Lucide icon name. + """ + return var_operation_return( + js_expression=f'{tag!s}.replaceAll("_", "-").replace(/(\\d)x-(\\d)/g, "$1x$2")', + var_type=str, + ) + + class Icon(LucideIconComponent): """An Icon component.""" @@ -74,7 +94,9 @@ def create(cls, *children, **props) -> Component: if not isinstance(tag_stringified, StringVar): msg = f"Icon name must be a string, got {tag_var._var_type}" raise TypeError(msg) - return DynamicIcon.create(name=tag_stringified.replace("_", "-"), **props) + return DynamicIcon.create( + name=dynamic_icon_name_operation(tag_stringified), **props + ) if tag not in LUCIDE_ICON_LIST: icons_sorted = sorted( diff --git a/pyi_hashes.json b/pyi_hashes.json index 9df583b7fa1..e5a9a57582d 100644 --- a/pyi_hashes.json +++ b/pyi_hashes.json @@ -39,7 +39,7 @@ "packages/reflex-components-core/src/reflex_components_core/react_router/dom.pyi": "7617635157b816f70354e2db98b38642", "packages/reflex-components-dataeditor/src/reflex_components_dataeditor/dataeditor.pyi": "1faf9b4e77e589f4d071483c14fe16f7", "packages/reflex-components-gridjs/src/reflex_components_gridjs/datatable.pyi": "2ce1c076ecf5c2fa4945b4abdbf2f91d", - "packages/reflex-components-lucide/src/reflex_components_lucide/icon.pyi": "0bbc5e33f2c2175c3ddcbf1b08dc8923", + "packages/reflex-components-lucide/src/reflex_components_lucide/icon.pyi": "78d8643641b2338f59bf0212a30b025a", "packages/reflex-components-markdown/src/reflex_components_markdown/markdown.pyi": "6759daf6e242e209d9298045c7b4456a", "packages/reflex-components-moment/src/reflex_components_moment/moment.pyi": "21c3396cb28b1ef6442c5a9588dc708b", "packages/reflex-components-plotly/src/reflex_components_plotly/plotly.pyi": "f411341e59a3065fee964747584f9fcf", diff --git a/tests/units/components/lucide/test_icon.py b/tests/units/components/lucide/test_icon.py index 033f42bd6c2..0bde617d07d 100644 --- a/tests/units/components/lucide/test_icon.py +++ b/tests/units/components/lucide/test_icon.py @@ -1,3 +1,5 @@ +import re + import pytest from reflex_base.utils import format from reflex_base.utils.imports import ImportVar @@ -87,6 +89,30 @@ def test_dynamic_icon_uses_dynamic_module(): assert not import_var.is_default +def test_dynamic_icon_name_normalizes_digit_x_digit(): + """Var-tagged icons drop the hyphen snake_case leaves in ``grid_2x_2``.""" + icon = Icon.create(Var("state_icon").to(str)) + assert ( + str(icon.render()["props"][0]) + == 'name:state_icon.replaceAll("_", "-").replace(/(\\d)x-(\\d)/g, "$1x$2")' + ) + + +@pytest.mark.parametrize("tag", LUCIDE_ICON_LIST) +def test_dynamic_icon_name_matches_lucide_name(tag): + """The runtime name conversion agrees with the verified per-icon file names.""" + # Mirror the JS expression emitted by dynamic_icon_name_operation. + name = re.sub(r"(\d)x-(\d)", r"\1x\2", tag.replace("_", "-")) + # "fingerprint" is a Lucide alias that DynamicIcon resolves under its own + # name; only the deep import needs the aliased file name. + expected = ( + "fingerprint" + if tag == "fingerprint" + else LUCIDE_ICON_FILENAME_OVERRIDE.get(tag, format.to_kebab_case(tag)) + ) + assert name == expected + + @pytest.mark.parametrize( ("tag", "expected"), [