diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 44bc817..933a067 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -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 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bdf884a..69806b9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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/ diff --git a/demo/assets/css/globals.css b/demo/assets/css/globals.css index e8af01f..e8f7904 100644 --- a/demo/assets/css/globals.css +++ b/demo/assets/css/globals.css @@ -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); } diff --git a/demo/demo/demo.py b/demo/demo/demo.py index 7570e46..ac27c23 100644 --- a/demo/demo/demo.py +++ b/demo/demo/demo.py @@ -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}"), diff --git a/reflex_ui/components/base/button.py b/reflex_ui/components/base/button.py index c092b49..a8efdbf 100644 --- a/reflex_ui/components/base/button.py +++ b/reflex_ui/components/base/button.py @@ -18,6 +18,7 @@ "outline-shadow", "secondary", "ghost", + "ghost-highlight", "link", "dark", ] @@ -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", }, diff --git a/reflex_ui/components/base/button.pyi b/reflex_ui/components/base/button.pyi index b8cd041..048d170 100644 --- a/reflex_ui/components/base/button.pyi +++ b/reflex_ui/components/base/button.pyi @@ -22,6 +22,7 @@ LiteralButtonVariant = Literal[ "outline-shadow", "secondary", "ghost", + "ghost-highlight", "link", "dark", ] @@ -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", }, @@ -71,6 +73,7 @@ class Button(BaseButton, CoreComponent): "dark", "destructive", "ghost", + "ghost-highlight", "link", "outline", "outline-shadow", @@ -83,6 +86,7 @@ class Button(BaseButton, CoreComponent): "dark", "destructive", "ghost", + "ghost-highlight", "link", "outline", "outline-shadow", @@ -359,6 +363,7 @@ class ButtonNamespace(ComponentNamespace): "dark", "destructive", "ghost", + "ghost-highlight", "link", "outline", "outline-shadow", @@ -371,6 +376,7 @@ class ButtonNamespace(ComponentNamespace): "dark", "destructive", "ghost", + "ghost-highlight", "link", "outline", "outline-shadow", diff --git a/reflex_ui/components/base/input.py b/reflex_ui/components/base/input.py index 24aeb7d..f1a02bd 100644 --- a/reflex_ui/components/base/input.py +++ b/reflex_ui/components/base/input.py @@ -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): @@ -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 @@ -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]: diff --git a/reflex_ui/components/base/input.pyi b/reflex_ui/components/base/input.pyi index 159d89d..419edc9 100644 --- a/reflex_ui/components/base/input.pyi +++ b/reflex_ui/components/base/input.pyi @@ -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 diff --git a/reflex_ui/components/base/tabs.py b/reflex_ui/components/base/tabs.py index 150c8c3..a4f02f4 100644 --- a/reflex_ui/components/base/tabs.py +++ b/reflex_ui/components/base/tabs.py @@ -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): @@ -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