Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(themes): Improve styling #24

Merged
merged 1 commit into from
Apr 9, 2024
Merged
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
34 changes: 19 additions & 15 deletions examples/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,21 +96,25 @@ def init_db() -> DB:


class Page(Component[AnyChildren, NoAttrs]):
styles = {
"body": {
"display": "flex",
"flex-direction": "column",
"align-items": "center",
"min-height": "100vh",
"margin": "0",
"font-family": "'Arial', sans-serif",
},
"main": {
"width": "80%",
"max-width": "800px",
"padding": "20px",
},
}
styles = style.use(
lambda theme: {
"body": {
"color": theme.colors.dark,
"background-color": theme.colors.white,
"display": "flex",
"flex-direction": "column",
"align-items": "center",
"min-height": "100vh",
"margin": "0",
"font-family": "'Arial', sans-serif",
},
"main": {
"width": "80%",
"max-width": "800px",
"padding": "20px",
},
}
)

@override
def render(self) -> BaseElement:
Expand Down
2 changes: 2 additions & 0 deletions examples/bulk_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ class Toast(span):
Toast.target: {
"background": theme.colors.success,
"margin": "10px 20px",
"padding": "5px 8px",
"border-radius": "3px",
"opacity": "0",
"transition": "opacity 3s ease-out",
},
Expand Down
5 changes: 3 additions & 2 deletions examples/click_to_edit.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Annotated, NotRequired, Self, override

from examples import Body, Header, Page, app, init_db
from ludic.catalog.buttons import ButtonDanger, ButtonPrimary
from ludic.catalog.buttons import Button, ButtonDanger, ButtonPrimary
from ludic.catalog.forms import FieldMeta, Form, create_fields
from ludic.catalog.items import Pairs
from ludic.catalog.quotes import Quote
Expand Down Expand Up @@ -71,7 +71,7 @@ async def put(cls, id: str, attrs: Parser[ContactAttrs]) -> Self:
def render(self) -> div:
return div(
Pairs(items=self.attrs.items()),
ButtonPrimary(
Button(
"Click To Edit",
hx_get=self.url_for(ContactForm),
),
Expand Down Expand Up @@ -99,4 +99,5 @@ def render(self) -> Form:
ButtonDanger("Cancel", hx_get=self.url_for(Contact)),
hx_put=self.url_for(Contact),
hx_target="this",
hx_swap="outerHTML",
)
8 changes: 6 additions & 2 deletions examples/edit_row.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

from examples import Body, Header, Page, app, init_db
from ludic.attrs import Attrs, HtmxAttrs
from ludic.catalog.buttons import ButtonPrimary, ButtonSecondary
from ludic.catalog.buttons import (
ButtonPrimary,
ButtonSecondary,
ButtonSuccess,
)
from ludic.catalog.forms import InputField
from ludic.catalog.quotes import Quote
from ludic.catalog.tables import ColumnMeta, Table, TableHead, TableRow
Expand Down Expand Up @@ -106,7 +110,7 @@ def render(self) -> TableRow:
InputField(name="email", value=self.attrs["email"]),
div(
ButtonSecondary("Cancel", hx_get=self.url_for(PersonRow)),
ButtonPrimary(
ButtonSuccess(
"Save",
hx_put=self.url_for(PersonRow),
hx_include="closest tr",
Expand Down
24 changes: 15 additions & 9 deletions ludic/catalog/buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ class Button(ComponentStrict[PrimitiveChildren, ButtonAttrs]):
lambda theme: {
"button.btn": {
"background-color": theme.colors.light,
"color": theme.colors.dark,
"color": theme.colors.black,
"margin": "8px 10px 8px 0px",
"padding": "10px 20px",
"border": f"1px solid {theme.colors.light.darken(0.2)}",
"padding": "10px 17px",
"border": f"1px solid {theme.colors.light.darken(0.1)}",
"border-radius": "4px",
"cursor": "pointer",
"font-size": theme.fonts.sizes.medium,
Expand Down Expand Up @@ -49,8 +49,9 @@ class ButtonPrimary(Button):
styles = style.use(
lambda theme: {
"button.btn-primary": {
"color": theme.colors.primary.readable(),
"background-color": theme.colors.primary,
"border-color": theme.colors.primary.darken(0.2),
"border-color": theme.colors.primary.darken(0.05),
}
}
)
Expand All @@ -66,8 +67,9 @@ class ButtonSecondary(Button):
styles = style.use(
lambda theme: {
"button.btn-secondary": {
"color": theme.colors.secondary.readable(),
"background-color": theme.colors.secondary,
"border-color": theme.colors.secondary.darken(0.2),
"border-color": theme.colors.secondary.darken(0.05),
}
}
)
Expand All @@ -83,8 +85,9 @@ class ButtonSuccess(Button):
styles = style.use(
lambda theme: {
"button.btn-success": {
"color": theme.colors.success.readable(),
"background-color": theme.colors.success,
"border-color": theme.colors.success.darken(0.2),
"border-color": theme.colors.success.darken(0.05),
}
}
)
Expand All @@ -100,8 +103,9 @@ class ButtonDanger(Button):
styles = style.use(
lambda theme: {
"button.btn-danger": {
"color": theme.colors.danger.readable(),
"background-color": theme.colors.danger,
"border-color": theme.colors.danger.darken(0.2),
"border-color": theme.colors.danger.darken(0.05),
}
}
)
Expand All @@ -117,8 +121,9 @@ class ButtonWarning(Button):
styles = style.use(
lambda theme: {
"button.btn-warning": {
"color": theme.colors.warning.readable(),
"background-color": theme.colors.warning,
"border-color": theme.colors.warning.darken(0.2),
"border-color": theme.colors.warning.darken(0.05),
}
}
)
Expand All @@ -134,8 +139,9 @@ class ButtonInfo(Button):
styles = style.use(
lambda theme: {
"button.btn-info": {
"color": theme.colors.info.readable(),
"background-color": theme.colors.info,
"border-color": theme.colors.info.darken(0.2),
"border-color": theme.colors.info.darken(0.05),
}
}
)
2 changes: 1 addition & 1 deletion ludic/catalog/quotes.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class Quote(ComponentStrict[str, QuoteAttrs]):
styles = style.use(
lambda theme: {
".quote": {
"margin-bottom": "40px", # type: ignore
"margin-bottom": "20px", # type: ignore
"blockquote": {
"background-color": theme.colors.light,
"border-left": f"8px solid {theme.colors.light.darken(0.1)}",
Expand Down
1 change: 1 addition & 0 deletions ludic/catalog/tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ class Table(ComponentStrict[THead, *tuple[TRow, ...], TableAttrs]):
"table.table": {
"width": "100%", # type: ignore
"border-collapse": "collapse", # type: ignore
"margin-bottom": "15px", # type: ignore
"thead": {
"background-color": theme.colors.light,
},
Expand Down
58 changes: 36 additions & 22 deletions ludic/styles/themes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Literal, Self, TypeVar

from .utils import darken_color, hex_to_rgb, lighten_color
from .utils import darken_color, hex_to_rgb, lighten_color, pick_readable_color_for

if TYPE_CHECKING:
from ludic.types import BaseElement
Expand Down Expand Up @@ -40,6 +40,17 @@ def lighten(self, factor: float = 0.5) -> Self:
"""
return type(self)(lighten_color(self, factor))

def readable(self) -> Self:
"""Get lighter or darker variant of the given color depending on the luminance.

Args:
color (str): Color to find the readable opposite for.

Returns:
str: Readable opposite of the given color.
"""
return type(self)(pick_readable_color_for(self))


class Size(str):
"""Size class."""
Expand Down Expand Up @@ -87,15 +98,16 @@ def dec(self, factor: float = 1) -> Self:
class Colors:
"""Colors for a theme."""

primary: Color = Color("#0d6efd")
secondary: Color = Color("#6c757d")
success: Color = Color("#198754")
info: Color = Color("#0dcaf0")
warning: Color = Color("#ffc107")
danger: Color = Color("#dc3545")
primary: Color = Color("#4ecdc4")
secondary: Color = Color("#fefefe")
success: Color = Color("#c7f464")
info: Color = Color("#fce303")
warning: Color = Color("#fc9003")
danger: Color = Color("#e32929")

light: Color = Color("#f8f8f8")
dark: Color = Color("#414549")

dark: Color = Color("#313539")
light: Color = Color("#f8f9fa")
white: Color = Color("#fff")
black: Color = Color("#222")

Expand Down Expand Up @@ -161,26 +173,28 @@ class DarkTheme(Theme):

name: str = "dark"

colors: Colors = field(
default_factory=lambda: Colors(
primary=Color("#0d6efd"),
secondary=Color("#6c757d"),
success=Color("#198754"),
info=Color("#0dcaf0"),
warning=Color("#ffc107"),
danger=Color("#dc3545"),
light=Color("#414549"),
dark=Color("#f8f8f8"),
white=Color("#000"),
black=Color("#fff"),
)
)


@dataclass
class LightTheme(Theme):
"""Light theme."""

name: str = "light"

colors: Colors = field(
default_factory=lambda: Colors(
primary=Color("#c2e7fd"),
secondary=Color("#fefefe"),
success=Color("#c9ffad"),
info=Color("#fff080"),
warning=Color("#ffc280"),
danger=Color("#ffaca1"),
light=Color("#f8f8f8"),
dark=Color("#414549"),
)
)


_DEFAULT_THEME: Theme = LightTheme()

Expand Down
16 changes: 15 additions & 1 deletion ludic/styles/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ def rgb_to_hex(rgb: tuple[float, float, float]) -> str:
return f"#{int(rgb[0]):02x}{int(rgb[1]):02x}{int(rgb[2]):02x}"


def pick_readable_color_for(color: str) -> str:
"""Get lighter or darker color variant of the given one depending on the luminance.

Args:
color (str): Color to find the readable opposite for.

Returns:
str: Readable opposite of the given color.
"""
rgb = color if isinstance(color, tuple) else hex_to_rgb(color)
_, luminance, _ = colorsys.rgb_to_hls(*rgb)
return "#000" if luminance > 140 else "#fff"


def scale_color(color: str, factor: float = 0.5) -> str:
"""Scale a color by a given factor.

Expand All @@ -54,7 +68,7 @@ def scale_color(color: str, factor: float = 0.5) -> str:
if factor < 1:
new_luminance = luminance * factor
else:
new_luminance = (255 - luminance) * (factor - 1)
new_luminance = luminance + (255 - luminance) * (factor - 1)

result = colorsys.hls_to_rgb(hue, min(255, max(1, new_luminance)), saturation)
return rgb_to_hex(result)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_themes.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def test_theme_colors() -> None:
assert theme.colors.black.lighten(1).rgb == (255, 255, 255)

assert theme.colors.light.darken(0.5).rgb == (119, 119, 119)
assert theme.colors.dark.lighten(0.5).rgb == (102, 102, 102)
assert theme.colors.dark.lighten(0.5).rgb == (153, 153, 153)

assert theme.colors.primary.darken(0.5).rgb == (97, 115, 126)

Expand Down