Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ jobs:
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Install UV
uses: astral-sh/setup-uv@v6
uses: astral-sh/setup-uv@v7
with:
python-version: "3.13"
activate-environment: true
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ fail_fast: true

repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.5
rev: v0.15.8
hooks:
- id: ruff-check
files: ^reflex_ui/
Expand Down
4 changes: 2 additions & 2 deletions demo/assets/css/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1114,10 +1114,10 @@
0 1px 4px 0 light-dark(rgba(0, 0, 0, 0.02), rgba(0, 0, 0, 0));
/* Radius */
--radius-ui-xxs: calc(var(--radius) - 0.25rem);
--radius-ui-xs: calc(var(--radius) - 0.125rem);
--radius-ui-xs: var(--radius);
--radius-ui-sm: var(--radius);
--radius-ui-md: calc(var(--radius) + 0.125rem);
--radius-ui-lg: calc(var(--radius) + 0.25rem);
--radius-ui-lg: calc(var(--radius) + 0.125rem);
--radius-ui-xl: calc(var(--radius) + 0.375rem);
--radius-ui-2xl: calc(var(--radius) + 0.5rem);
}
Expand Down
23 changes: 23 additions & 0 deletions demo/demo/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,29 @@ def index() -> rx.Component:
),
content="Seriously, click me",
),
ui.input(
icon="SmileIcon",
placeholder="Hello",
),
ui.textarea(
placeholder="Hello",
),
ui.tabs.root(
ui.tabs.list(
ui.tabs.tab(
rx.el.span("Item 1", class_name="px-1"),
value="item-1",
size="sm",
),
ui.tabs.tab("Item 2", value="item-2", size="sm"),
ui.tabs.tab("Item 3", value="item-3", size="sm"),
ui.tabs.indicator(size="sm"),
size="sm",
),
ui.tabs.panel("Item 1", value="item-1"),
ui.tabs.panel("Item 2", value="item-2"),
ui.tabs.panel("Item 3", value="item-3"),
),
ui.checkbox(
label="Click me",
on_checked_change=lambda value: rx.toast.success(f"Value: {value}"),
Expand Down
4 changes: 3 additions & 1 deletion reflex_ui/components/base/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"outline-shadow",
"secondary",
"ghost",
"ghost-highlight",
"link",
"dark",
]
Expand All @@ -33,9 +34,10 @@
"primary-bordered": "bg-primary-9 text-primary-contrast hover:bg-primary-10 shadow-button-bordered disabled:shadow-none",
"destructive": "bg-destructive-9 hover:bg-destructive-10 text-primary-contrast",
"outline": "border border-secondary-a4 bg-secondary-1 hover:bg-secondary-3 text-secondary-12",
"outline-shadow": "dark:border dark:border-secondary-a4 bg-white dark:bg-secondary-1 hover:bg-secondary-3 text-secondary-12 shadow-button-outline disabled:shadow-none",
"outline-shadow": "dark:shadow-[0_1px_0_0_rgba(255,255,255,0.08)_inset] bg-white hover:bg-secondary-2 dark:bg-secondary-3 dark:hover:bg-secondary-4 text-secondary-12 shadow-button-outline disabled:shadow-none",
"secondary": "bg-secondary-4 text-secondary-12 hover:bg-secondary-5",
"ghost": "hover:bg-secondary-3 text-secondary-11",
"ghost-highlight": "text-secondary-12 hover:text-primary-9",
"link": "text-secondary-12 underline-offset-4 hover:underline",
"dark": "bg-secondary-12 text-secondary-1 hover:bg-secondary-12/80",
},
Expand Down
8 changes: 7 additions & 1 deletion reflex_ui/components/base/button.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ LiteralButtonVariant = Literal[
"outline-shadow",
"secondary",
"ghost",
"ghost-highlight",
"link",
"dark",
]
Expand All @@ -35,9 +36,10 @@ BUTTON_VARIANTS = {
"primary-bordered": "bg-primary-9 text-primary-contrast hover:bg-primary-10 shadow-button-bordered disabled:shadow-none",
"destructive": "bg-destructive-9 hover:bg-destructive-10 text-primary-contrast",
"outline": "border border-secondary-a4 bg-secondary-1 hover:bg-secondary-3 text-secondary-12",
"outline-shadow": "dark:border dark:border-secondary-a4 bg-white dark:bg-secondary-1 hover:bg-secondary-3 text-secondary-12 shadow-button-outline disabled:shadow-none",
"outline-shadow": "dark:shadow-[0_1px_0_0_rgba(255,255,255,0.08)_inset] bg-white hover:bg-secondary-2 dark:bg-secondary-3 dark:hover:bg-secondary-4 text-secondary-12 shadow-button-outline disabled:shadow-none",
"secondary": "bg-secondary-4 text-secondary-12 hover:bg-secondary-5",
"ghost": "hover:bg-secondary-3 text-secondary-11",
"ghost-highlight": "text-secondary-12 hover:text-primary-9",
"link": "text-secondary-12 underline-offset-4 hover:underline",
"dark": "bg-secondary-12 text-secondary-1 hover:bg-secondary-12/80",
},
Expand Down Expand Up @@ -71,6 +73,7 @@ class Button(BaseButton, CoreComponent):
"dark",
"destructive",
"ghost",
"ghost-highlight",
"link",
"outline",
"outline-shadow",
Expand All @@ -83,6 +86,7 @@ class Button(BaseButton, CoreComponent):
"dark",
"destructive",
"ghost",
"ghost-highlight",
"link",
"outline",
"outline-shadow",
Expand Down Expand Up @@ -359,6 +363,7 @@ class ButtonNamespace(ComponentNamespace):
"dark",
"destructive",
"ghost",
"ghost-highlight",
"link",
"outline",
"outline-shadow",
Expand All @@ -371,6 +376,7 @@ class ButtonNamespace(ComponentNamespace):
"dark",
"destructive",
"ghost",
"ghost-highlight",
"link",
"outline",
"outline-shadow",
Expand Down
8 changes: 4 additions & 4 deletions reflex_ui/components/base/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
class ClassNames:
"""Class names for input components."""

INPUT = "outline-none bg-transparent text-secondary-12 placeholder:text-secondary-9 text-sm leading-normal peer disabled:text-secondary-8 disabled:placeholder:text-secondary-8 w-full data-[disabled]:pointer-events-none font-medium"
DIV = "flex flex-row items-center focus-within:shadow-[0px_0px_0px_2px_var(--primary-4)] focus-within:border-primary-a6 not-data-[invalid]:focus-within:hover:border-primary-a6 bg-secondary-1 shrink-0 border border-secondary-a4 hover:border-secondary-a6 transition-[color,box-shadow] text-secondary-9 [&_svg]:pointer-events-none has-data-[disabled]:border-secondary-4 has-data-[disabled]:bg-secondary-3 has-data-[disabled]:text-secondary-8 has-data-[disabled]:cursor-not-allowed cursor-text has-data-[invalid]:border-destructive-10 has-data-[invalid]:focus-within:border-destructive-a11 has-data-[invalid]:focus-within:shadow-[0px_0px_0px_2px_var(--destructive-4)] has-data-[invalid]:hover:border-destructive-a11"
INPUT = "outline-none bg-transparent text-secondary-12 placeholder:text-secondary-10 text-sm leading-normal peer disabled:text-secondary-8 disabled:placeholder:text-secondary-8 w-full data-[disabled]:pointer-events-none font-medium"
DIV = "flex flex-row items-center focus-within:shadow-[0px_0px_0px_2px_var(--primary-4)] focus-within:border-primary-a6 not-data-[invalid]:focus-within:hover:border-primary-a6 bg-white dark:bg-secondary-3 shrink-0 border border-secondary-4 hover:border-secondary-a6 transition-[color,box-shadow] text-secondary-9 [&_svg]:pointer-events-none has-data-[disabled]:border-secondary-4 has-data-[disabled]:bg-secondary-3 has-data-[disabled]:text-secondary-8 has-data-[disabled]:cursor-not-allowed cursor-text has-data-[invalid]:border-destructive-10 has-data-[invalid]:focus-within:border-destructive-a11 has-data-[invalid]:focus-within:shadow-[0px_0px_0px_2px_var(--destructive-4)] has-data-[invalid]:hover:border-destructive-a11 shadow-[0_1px_2px_0_rgba(0,0,0,0.02),0_1px_4px_0_rgba(0,0,0,0.02)] dark:shadow-none dark:border-secondary-5"


class InputBaseComponent(BaseUIComponent):
Expand Down Expand Up @@ -145,7 +145,7 @@ def create(cls, *children, **props) -> BaseUIComponent:
return Div.create( # pyright: ignore[reportReturnType]
(
Span.create(
hi(icon, class_name="text-secondary-9 size-4 pointer-events-none"),
hi(icon, class_name="text-secondary-12 size-4 pointer-events-none"),
aria_hidden="true",
)
if icon
Expand All @@ -171,7 +171,7 @@ def _create_clear_button(id: str, clear_events: list[EventHandler]) -> Button:
*clear_events,
],
tab_index=-1,
class_name="opacity-100 peer-placeholder-shown:opacity-0 hover:text-secondary-12 transition-colors peer-placeholder-shown:pointer-events-none peer-disabled:pointer-events-none peer-disabled:opacity-0 h-full",
class_name="opacity-100 peer-placeholder-shown:opacity-0 hover:text-secondary-12 transition-colors peer-placeholder-shown:pointer-events-none peer-disabled:pointer-events-none peer-disabled:opacity-0 h-full text-secondary-11",
)

def _exclude_props(self) -> list[str]:
Expand Down
4 changes: 2 additions & 2 deletions reflex_ui/components/base/input.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ DEFAULT_INPUT_ATTRS = {
}

class ClassNames:
INPUT = "outline-none bg-transparent text-secondary-12 placeholder:text-secondary-9 text-sm leading-normal peer disabled:text-secondary-8 disabled:placeholder:text-secondary-8 w-full data-[disabled]:pointer-events-none font-medium"
DIV = "flex flex-row items-center focus-within:shadow-[0px_0px_0px_2px_var(--primary-4)] focus-within:border-primary-a6 not-data-[invalid]:focus-within:hover:border-primary-a6 bg-secondary-1 shrink-0 border border-secondary-a4 hover:border-secondary-a6 transition-[color,box-shadow] text-secondary-9 [&_svg]:pointer-events-none has-data-[disabled]:border-secondary-4 has-data-[disabled]:bg-secondary-3 has-data-[disabled]:text-secondary-8 has-data-[disabled]:cursor-not-allowed cursor-text has-data-[invalid]:border-destructive-10 has-data-[invalid]:focus-within:border-destructive-a11 has-data-[invalid]:focus-within:shadow-[0px_0px_0px_2px_var(--destructive-4)] has-data-[invalid]:hover:border-destructive-a11"
INPUT = "outline-none bg-transparent text-secondary-12 placeholder:text-secondary-10 text-sm leading-normal peer disabled:text-secondary-8 disabled:placeholder:text-secondary-8 w-full data-[disabled]:pointer-events-none font-medium"
DIV = "flex flex-row items-center focus-within:shadow-[0px_0px_0px_2px_var(--primary-4)] focus-within:border-primary-a6 not-data-[invalid]:focus-within:hover:border-primary-a6 bg-white dark:bg-secondary-3 shrink-0 border border-secondary-4 hover:border-secondary-a6 transition-[color,box-shadow] text-secondary-9 [&_svg]:pointer-events-none has-data-[disabled]:border-secondary-4 has-data-[disabled]:bg-secondary-3 has-data-[disabled]:text-secondary-8 has-data-[disabled]:cursor-not-allowed cursor-text has-data-[invalid]:border-destructive-10 has-data-[invalid]:focus-within:border-destructive-a11 has-data-[invalid]:focus-within:shadow-[0px_0px_0px_2px_var(--destructive-4)] has-data-[invalid]:hover:border-destructive-a11 shadow-[0_1px_2px_0_rgba(0,0,0,0.02),0_1px_4px_0_rgba(0,0,0,0.02)] dark:shadow-none dark:border-secondary-5"

class InputBaseComponent(BaseUIComponent):
@property
Expand Down
88 changes: 82 additions & 6 deletions reflex_ui/components/base/tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,63 @@
from reflex_ui.components.base_ui import PACKAGE_NAME, BaseUIComponent

LiteralOrientation = Literal["horizontal", "vertical"]
LiteralTabsSize = Literal["sm", "md", "lg"]

DEFAULT_LIST_CLASS_NAME = "bg-secondary-1 inline-flex items-center justify-start relative z-0 shadow-button-outline dark:border dark:border-secondary-4"
DEFAULT_TAB_CLASS_NAME = "justify-center items-center inline-flex font-medium text-secondary-11 cursor-pointer z-[1] hover:text-primary-9 transition-color text-nowrap data-[active]:text-secondary-12 data-[disabled]:cursor-not-allowed data-[disabled]:text-secondary-8 text-sm"
DEFAULT_INDICATOR_CLASS_NAME = "absolute left-0 inset-y-0 my-0.5 -z-1 w-(--active-tab-width) translate-x-(--active-tab-left) transition-all duration-200 ease-in-out dark:shadow-[0_1px_0_0_rgba(255,255,255,0.08)_inset] bg-white dark:bg-secondary-3 text-secondary-12 shadow-button-outline"

TABS_SIZES = {
"list": {
"sm": "p-0.5 rounded-ui-md gap-0.5",
"md": "p-0.5 rounded-ui-md gap-0.5",
"lg": "p-0.5 rounded-ui-md gap-0.5",
},
"tab": {
"sm": "h-7 px-1.5 rounded-ui-sm gap-1",
"md": "h-8 px-2 rounded-ui-sm gap-1.5",
"lg": "h-9 px-2.5 rounded-ui-sm gap-2",
},
"indicator": {
"sm": "rounded-ui-sm",
"md": "rounded-ui-sm",
"lg": "rounded-ui-sm",
},
}


def _validate_tabs_size(kind: Literal["list", "tab", "indicator"], size: str) -> None:
allowed = TABS_SIZES[kind]
if size not in allowed:
available = ", ".join(allowed.keys())
msg = f"Invalid tabs {kind} size: {size!r}. Available sizes: {available}"
raise ValueError(msg)


class ClassNames:
"""Class names for tabs components."""

ROOT = "flex flex-col gap-2"
LIST = "bg-secondary-3 inline-flex gap-1 p-1 items-center justify-start rounded-ui-md relative z-0"
TAB = "h-7 px-1.5 rounded-ui-sm justify-center items-center gap-1.5 inline-flex text-sm font-medium text-secondary-11 cursor-pointer z-[1] hover:text-secondary-12 transition-color text-nowrap data-[active]:text-secondary-12 data-[disabled]:cursor-not-allowed data-[disabled]:text-secondary-8"
INDICATOR = "absolute top-1/2 left-0 -z-1 h-7 w-(--active-tab-width) -translate-y-1/2 translate-x-(--active-tab-left) rounded-ui-sm bg-secondary-1 shadow-small transition-all duration-200 ease-in-out"
LIST = f"{DEFAULT_LIST_CLASS_NAME} {TABS_SIZES['list']['sm']}"
TAB = f"{DEFAULT_TAB_CLASS_NAME} {TABS_SIZES['tab']['sm']}"
INDICATOR = f"{DEFAULT_INDICATOR_CLASS_NAME} {TABS_SIZES['indicator']['sm']}"
PANEL = "flex flex-col gap-2"
SIZES = TABS_SIZES

@staticmethod
def for_list(size: str = "sm") -> str:
"""Return combined class string for the given size."""
return f"{DEFAULT_LIST_CLASS_NAME} {TABS_SIZES['list'][size]}"

@staticmethod
def for_tab(size: str = "sm") -> str:
"""Return combined class string for the given size."""
return f"{DEFAULT_TAB_CLASS_NAME} {TABS_SIZES['tab'][size]}"

@staticmethod
def for_indicator(size: str = "sm") -> str:
"""Return combined class string for the given size."""
return f"{DEFAULT_INDICATOR_CLASS_NAME} {TABS_SIZES['indicator'][size]}"


class TabsBaseComponent(BaseUIComponent):
Expand Down Expand Up @@ -72,13 +119,22 @@ class TabsList(TabsBaseComponent):
# 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 size of the tabs list. Defaults to "sm".
size: Var[LiteralTabsSize]

@classmethod
def create(cls, *children, **props) -> BaseUIComponent:
"""Create the tabs list component."""
size = props.pop("size", "sm")
_validate_tabs_size("list", size)
props["data-slot"] = "tabs-list"
cls.set_class_name(ClassNames.LIST, props)
list_classes = f"{DEFAULT_LIST_CLASS_NAME} {TABS_SIZES['list'][size]}"
cls.set_class_name(list_classes, props)
return super().create(*children, **props)

def _exclude_props(self) -> list[str]:
return [*super()._exclude_props(), "size"]


class TabsTab(TabsBaseComponent):
"""An individual interactive tab button that toggles the corresponding panel. Renders a <button> element."""
Expand All @@ -97,13 +153,22 @@ class TabsTab(TabsBaseComponent):
# The render prop
render_: Var[Component]

# The size of the tab. Defaults to "sm".
size: Var[LiteralTabsSize]

@classmethod
def create(cls, *children, **props) -> BaseUIComponent:
"""Create the tabs tab component."""
size = props.pop("size", "sm")
_validate_tabs_size("tab", size)
props["data-slot"] = "tabs-tab"
cls.set_class_name(ClassNames.TAB, props)
tab_classes = f"{DEFAULT_TAB_CLASS_NAME} {TABS_SIZES['tab'][size]}"
cls.set_class_name(tab_classes, props)
return super().create(*children, **props)

def _exclude_props(self) -> list[str]:
return [*super()._exclude_props(), "size"]


class TabsIndicator(TabsBaseComponent):
"""A visual indicator that can be styled to match the position of the currently active tab. Renders a <span> element."""
Expand All @@ -116,13 +181,24 @@ class TabsIndicator(TabsBaseComponent):
# The render prop
render_: Var[Component]

# The size of the indicator. Defaults to "sm".
size: Var[LiteralTabsSize]

@classmethod
def create(cls, *children, **props) -> BaseUIComponent:
"""Create the tabs indicator component."""
size = props.pop("size", "sm")
_validate_tabs_size("indicator", size)
props["data-slot"] = "tabs-indicator"
cls.set_class_name(ClassNames.INDICATOR, props)
indicator_classes = (
f"{DEFAULT_INDICATOR_CLASS_NAME} {TABS_SIZES['indicator'][size]}"
)
cls.set_class_name(indicator_classes, props)
return super().create(*children, **props)

def _exclude_props(self) -> list[str]:
return [*super()._exclude_props(), "size"]


class TabsPanel(TabsBaseComponent):
"""A panel displayed when the corresponding tab is active. Renders a <div> element."""
Expand Down
Loading
Loading