Skip to content

Commit 9d97154

Browse files
devin-ai-integration[bot]carlosreflexcarlosabadia
authored
Add preview card component from Base UI (#27)
* Add preview card component from Base UI - Create PreviewCard component with all sub-components (Root, Trigger, Portal, Positioner, Popup, Arrow, Backdrop) - Implement HighLevelPreviewCard wrapper with trigger and content props as requested - Follow existing component patterns from popover and tooltip components - Add component to lazy loading system in __init__.py - Add demo usage in demo application - All prop descriptions sourced from official Base UI documentation Co-Authored-By: Carlos Cutillas <[email protected]> * Add missing prop descriptions to PreviewCardRoot - Add description for default_open prop - Add description for open prop - Add description for on_open_change prop - Add description for delay prop with default value - Follow established comment format for consistency Co-Authored-By: Carlos Cutillas <[email protected]> * Update all prop descriptions with official Base UI documentation - Source all prop descriptions from official Base UI JSON documentation files - Update PreviewCardRoot, PreviewCardTrigger, PreviewCardBackdrop, PreviewCardPortal, PreviewCardPositioner, PreviewCardPopup, and PreviewCardArrow components - Add render prop descriptions for all components that support it - Include default values where specified in official docs - Follow established comment format for consistency - Maintain existing functionality while improving documentation accuracy Co-Authored-By: Carlos Cutillas <[email protected]> * updates * update --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: Carlos Cutillas <[email protected]> Co-authored-by: carlosabadia <[email protected]>
1 parent 1e04e20 commit 9d97154

File tree

6 files changed

+835
-1
lines changed

6 files changed

+835
-1
lines changed

demo/rxconfig.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,8 @@
33
config = rx.Config(
44
app_name="demo",
55
telemetry_enabled=False,
6-
plugins=[rx.plugins.TailwindV4Plugin()],
6+
plugins=[
7+
rx.plugins.SitemapPlugin(),
8+
rx.plugins.TailwindV4Plugin(),
9+
],
710
)

reflex_ui/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"components.base.menu": ["menu"],
1616
"components.base.navigation_menu": ["navigation_menu"],
1717
"components.base.popover": ["popover"],
18+
"components.base.preview_card": ["preview_card"],
1819
"components.base.scroll_area": ["scroll_area"],
1920
"components.base.select": ["select"],
2021
"components.base.skeleton": ["skeleton"],

reflex_ui/__init__.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ from .components.base.link import link
1717
from .components.base.menu import menu
1818
from .components.base.navigation_menu import navigation_menu
1919
from .components.base.popover import popover
20+
from .components.base.preview_card import preview_card
2021
from .components.base.scroll_area import scroll_area
2122
from .components.base.select import select
2223
from .components.base.skeleton import skeleton
@@ -44,6 +45,7 @@ _REFLEX_UI_MAPPING = {
4445
"components.base.menu": ["menu"],
4546
"components.base.navigation_menu": ["navigation_menu"],
4647
"components.base.popover": ["popover"],
48+
"components.base.preview_card": ["preview_card"],
4749
"components.base.scroll_area": ["scroll_area"],
4850
"components.base.select": ["select"],
4951
"components.base.skeleton": ["skeleton"],
@@ -82,6 +84,7 @@ __all__ = [
8284
"menu",
8385
"navigation_menu",
8486
"popover",
87+
"preview_card",
8588
"scroll_area",
8689
"select",
8790
"skeleton",

reflex_ui/components/base/__init__.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ from .link import link
1717
from .menu import menu
1818
from .navigation_menu import navigation_menu
1919
from .popover import popover
20+
from .preview_card import preview_card
2021
from .scroll_area import scroll_area
2122
from .select import select
2223
from .skeleton import skeleton
@@ -46,6 +47,7 @@ __all__ = [
4647
"menu",
4748
"navigation_menu",
4849
"popover",
50+
"preview_card",
4951
"scroll_area",
5052
"select",
5153
"skeleton",
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
"""Custom preview card component."""
2+
3+
from typing import Literal
4+
5+
from reflex.components.component import Component, ComponentNamespace
6+
from reflex.event import EventHandler, passthrough_event_spec
7+
from reflex.utils.imports import ImportVar
8+
from reflex.vars.base import Var
9+
10+
from reflex_ui.components.base_ui import PACKAGE_NAME, BaseUIComponent
11+
from reflex_ui.utils.twmerge import cn
12+
13+
LiteralAlign = Literal["start", "center", "end"]
14+
LiteralSide = Literal["bottom", "inline-end", "inline-start", "left", "right", "top"]
15+
LiteralPosition = Literal["absolute", "fixed"]
16+
17+
18+
class ClassNames:
19+
"""Class names for preview card components."""
20+
21+
ROOT = ""
22+
TRIGGER = ""
23+
BACKDROP = ""
24+
PORTAL = ""
25+
POSITIONER = ""
26+
POPUP = "origin-(--transform-origin) rounded-xl p-4 border border-secondary-a4 bg-secondary-1 shadow-large transition-[transform,scale,opacity] data-[ending-style]:scale-95 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0 outline-none min-w-64 flex flex-col gap-3"
27+
ARROW = "data-[side=bottom]:top-[-8px] data-[side=left]:right-[-13px] data-[side=left]:rotate-90 data-[side=right]:left-[-13px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-8px] data-[side=top]:rotate-180"
28+
29+
30+
class PreviewCardBaseComponent(BaseUIComponent):
31+
"""Base component for preview card components."""
32+
33+
library = f"{PACKAGE_NAME}/preview-card"
34+
35+
@property
36+
def import_var(self):
37+
"""Return the import variable for the preview card component."""
38+
return ImportVar(tag="PreviewCard", package_path="", install=False)
39+
40+
41+
class PreviewCardRoot(PreviewCardBaseComponent):
42+
"""Groups all parts of the preview card. Doesn't render its own HTML element."""
43+
44+
tag = "PreviewCard.Root"
45+
46+
# Whether the preview card is initially open. To render a controlled preview card, use the `open` prop instead. Defaults to false.
47+
default_open: Var[bool]
48+
49+
# Whether the preview card is currently open.
50+
open: Var[bool]
51+
52+
# Event handler called when the preview card is opened or closed.
53+
on_open_change: EventHandler[passthrough_event_spec(bool, dict, str)]
54+
55+
# Event handler called after any animations complete when the preview card is opened or closed.
56+
on_open_change_complete: EventHandler[passthrough_event_spec(bool)]
57+
58+
# How long to wait before the preview card opens. Specified in milliseconds. Defaults to 600.
59+
delay: Var[int]
60+
61+
# How long to wait before closing the preview card that was opened on hover. Specified in milliseconds. Defaults to 300.
62+
close_delay: Var[int]
63+
64+
@classmethod
65+
def create(cls, *children, **props) -> BaseUIComponent:
66+
"""Create the preview card root component."""
67+
props["data-slot"] = "preview-card"
68+
return super().create(*children, **props)
69+
70+
71+
class PreviewCardTrigger(PreviewCardBaseComponent):
72+
"""A button that opens the preview card. Renders a <button> element."""
73+
74+
tag = "PreviewCard.Trigger"
75+
76+
render_: Var[Component]
77+
78+
@classmethod
79+
def create(cls, *children, **props) -> BaseUIComponent:
80+
"""Create the preview card trigger component."""
81+
props["data-slot"] = "preview-card-trigger"
82+
cls.set_class_name(ClassNames.TRIGGER, props)
83+
return super().create(*children, **props)
84+
85+
86+
class PreviewCardBackdrop(PreviewCardBaseComponent):
87+
"""An overlay displayed beneath the popup. Renders a <div> element."""
88+
89+
tag = "PreviewCard.Backdrop"
90+
91+
# The render prop
92+
render_: Var[Component]
93+
94+
@classmethod
95+
def create(cls, *children, **props) -> BaseUIComponent:
96+
"""Create the preview card backdrop component."""
97+
props["data-slot"] = "preview-card-backdrop"
98+
cls.set_class_name(ClassNames.BACKDROP, props)
99+
return super().create(*children, **props)
100+
101+
102+
class PreviewCardPortal(PreviewCardBaseComponent):
103+
"""A portal element that moves the popup to a different part of the DOM. By default, the portal element is appended to <body>."""
104+
105+
tag = "PreviewCard.Portal"
106+
107+
# A parent element to render the portal element into.
108+
container: Var[str]
109+
110+
# Whether to keep the portal mounted in the DOM while the popup is hidden. Defaults to false.
111+
keep_mounted: Var[bool]
112+
113+
114+
class PreviewCardPositioner(PreviewCardBaseComponent):
115+
"""Positions the preview card against the trigger. Renders a <div> element."""
116+
117+
tag = "PreviewCard.Positioner"
118+
119+
# Determines how to handle collisions when positioning the popup.
120+
collision_avoidance: Var[str]
121+
122+
# How to align the popup relative to the specified side. Defaults to center.
123+
align: Var[LiteralAlign]
124+
125+
# Additional offset along the alignment axis in pixels. Also accepts a function that returns the offset to read the dimensions of the popup. Defaults to 0.
126+
align_offset: Var[int]
127+
128+
# Which side of the anchor element to align the popup against. May automatically change to avoid collisions. Defaults to bottom.
129+
side: Var[LiteralSide]
130+
131+
# Distance between the anchor and the popup in pixels. Also accepts a function that returns the distance to read the dimensions of the popup. Defaults to 0.
132+
side_offset: Var[int]
133+
134+
# Minimum distance to maintain between the arrow and the edges of the popup. Use it to prevent the arrow element from hanging out of the rounded corners of a popup. Defaults to 5.
135+
arrow_padding: Var[int]
136+
137+
# An element to position the popup against. By default, the popup will be positioned against the trigger.
138+
anchor: Var[str]
139+
140+
# An element or a rectangle that delimits the area that the popup is confined to. Defaults to clipping-ancestors.
141+
collision_boundary: Var[str]
142+
143+
# Additional space to maintain from the edge of the collision boundary. Defaults to 5.
144+
collision_padding: Var[int | list[int]]
145+
146+
# Whether to maintain the popup in the viewport after the anchor element was scrolled out of view. Defaults to false.
147+
sticky: Var[bool]
148+
149+
# Determines which CSS position property to use. Defaults to absolute.
150+
position_method: Var[LiteralPosition]
151+
152+
# Whether the popup tracks any layout shift of its positioning anchor. Defaults to true.
153+
track_anchor: Var[bool]
154+
155+
# The render prop
156+
render_: Var[Component]
157+
158+
@classmethod
159+
def create(cls, *children, **props) -> BaseUIComponent:
160+
"""Create the preview card positioner component."""
161+
props["data-slot"] = "preview-card-positioner"
162+
props.setdefault("side_offset", 8)
163+
cls.set_class_name(ClassNames.POSITIONER, props)
164+
return super().create(*children, **props)
165+
166+
167+
class PreviewCardPopup(PreviewCardBaseComponent):
168+
"""A container for the preview card contents. Renders a <div> element."""
169+
170+
tag = "PreviewCard.Popup"
171+
172+
# The render prop
173+
render_: Var[Component]
174+
175+
@classmethod
176+
def create(cls, *children, **props) -> BaseUIComponent:
177+
"""Create the preview card popup component."""
178+
props["data-slot"] = "preview-card-popup"
179+
cls.set_class_name(ClassNames.POPUP, props)
180+
return super().create(*children, **props)
181+
182+
183+
class PreviewCardArrow(PreviewCardBaseComponent):
184+
"""Displays an element positioned against the preview card anchor. Renders a <div> element."""
185+
186+
tag = "PreviewCard.Arrow"
187+
188+
# The render prop
189+
render_: Var[Component]
190+
191+
@classmethod
192+
def create(cls, *children, **props) -> BaseUIComponent:
193+
"""Create the preview card arrow component."""
194+
props["data-slot"] = "preview-card-arrow"
195+
cls.set_class_name(ClassNames.ARROW, props)
196+
return super().create(*children, **props)
197+
198+
199+
class HighLevelPreviewCard(PreviewCardRoot):
200+
"""High level wrapper for the PreviewCard component."""
201+
202+
trigger: Var[Component | None]
203+
content: Var[str | Component | None]
204+
205+
# Props for different component parts
206+
_positioner_props = {
207+
"align",
208+
"align_offset",
209+
"side",
210+
"side_offset",
211+
"arrow_padding",
212+
"collision_padding",
213+
"collision_boundary",
214+
"sticky",
215+
"position_method",
216+
"track_anchor",
217+
"anchor",
218+
"collision_avoidance",
219+
}
220+
_portal_props = {"container", "keep_mounted"}
221+
222+
@classmethod
223+
def create(cls, *children, **props) -> BaseUIComponent:
224+
"""Create a preview card component.
225+
226+
Args:
227+
*children: Additional children to include in the preview card.
228+
**props: Additional properties to apply to the preview card component.
229+
230+
Returns:
231+
The preview card component.
232+
"""
233+
# Extract props for different parts
234+
positioner_props = {
235+
k: props.pop(k) for k in cls._positioner_props & props.keys()
236+
}
237+
portal_props = {k: props.pop(k) for k in cls._portal_props & props.keys()}
238+
239+
trigger = props.pop("trigger", None)
240+
content = props.pop("content", None)
241+
class_name = props.pop("class_name", "")
242+
243+
return PreviewCardRoot.create(
244+
PreviewCardTrigger.create(render_=trigger) if trigger else None,
245+
PreviewCardPortal.create(
246+
PreviewCardPositioner.create(
247+
PreviewCardPopup.create(
248+
content,
249+
*children,
250+
class_name=cn(ClassNames.POPUP, class_name),
251+
),
252+
**positioner_props,
253+
),
254+
**portal_props,
255+
),
256+
**props,
257+
)
258+
259+
def _exclude_props(self) -> list[str]:
260+
return [
261+
*super()._exclude_props(),
262+
"trigger",
263+
"content",
264+
]
265+
266+
267+
class PreviewCard(ComponentNamespace):
268+
"""Namespace for PreviewCard components."""
269+
270+
root = staticmethod(PreviewCardRoot.create)
271+
trigger = staticmethod(PreviewCardTrigger.create)
272+
backdrop = staticmethod(PreviewCardBackdrop.create)
273+
portal = staticmethod(PreviewCardPortal.create)
274+
positioner = staticmethod(PreviewCardPositioner.create)
275+
popup = staticmethod(PreviewCardPopup.create)
276+
arrow = staticmethod(PreviewCardArrow.create)
277+
__call__ = staticmethod(HighLevelPreviewCard.create)
278+
279+
280+
preview_card = PreviewCard()

0 commit comments

Comments
 (0)