diff --git a/common/components.py b/common/components.py
new file mode 100644
index 0000000..a969397
--- /dev/null
+++ b/common/components.py
@@ -0,0 +1,117 @@
+from random import choices as random_choices
+from string import ascii_lowercase
+from typing import Any, Callable
+
+from django.template.loader import render_to_string
+from django.urls import NoReverseMatch, reverse
+from django.utils.safestring import mark_safe
+
+HTMLAttribute = tuple[str, str | int | bool]
+HTMLTag = str
+
+
+def Component(
+ attributes: list[HTMLAttribute] = [],
+ children: list[HTMLTag] | HTMLTag = [],
+ template: str = "",
+ tag_name: str = "",
+) -> HTMLTag:
+ if not tag_name and not template:
+ raise ValueError("One of template or tag_name is required.")
+ if isinstance(children, str):
+ children = [children]
+ childrenBlob = "\n".join(children)
+ attributesList = [f'{name} = "{value}"' for name, value in attributes]
+ attributesBlob = " ".join(attributesList)
+ tag: str = ""
+ if tag_name != "":
+ tag = f"{childrenBlob}"
+ elif template != "":
+ tag = render_to_string(
+ template,
+ {name: value for name, value in attributes}
+ | {"slot": mark_safe("\n".join(children))},
+ )
+ return mark_safe(tag)
+
+
+def randomid(seed: str = "", length: int = 10) -> str:
+ return seed + "".join(random_choices(ascii_lowercase, k=length))
+
+
+def Popover(
+ popover_content: str,
+ wrapped_content: str = "",
+ children: list[HTMLTag] = [],
+ attributes: list[HTMLAttribute] = [],
+) -> str:
+ if not wrapped_content and not children:
+ raise ValueError("One of wrapped_content or children is required.")
+ id = randomid()
+ return Component(
+ attributes=attributes
+ + [
+ ("id", id),
+ ("wrapped_content", wrapped_content),
+ ("popover_content", popover_content),
+ ],
+ children=children,
+ template="cotton/popover.html",
+ )
+
+
+def A(
+ attributes: list[HTMLAttribute] = [],
+ children: list[HTMLTag] | HTMLTag = [],
+ url: str | Callable[..., Any] = "",
+):
+ """
+ Returns the HTML tag "a".
+ "url" can either be:
+ - URL (string)
+ - path name passed to reverse() (string)
+ - function
+ """
+ additional_attributes = []
+ if url:
+ if type(url) is str:
+ try:
+ url_result = reverse(url)
+ except NoReverseMatch:
+ url_result = url
+ elif callable(url):
+ url_result = url()
+ else:
+ raise TypeError("'url' is neither str nor function.")
+ additional_attributes = [("href", url_result)]
+ return Component(
+ tag_name="a", attributes=attributes + additional_attributes, children=children
+ )
+
+
+def Button(
+ attributes: list[HTMLAttribute] = [],
+ children: list[HTMLTag] | HTMLTag = [],
+ size: str = "base",
+ icon: bool = False,
+ color: str = "blue",
+):
+ return Component(
+ template="cotton/button.html",
+ attributes=attributes + [("size", size), ("icon", icon), ("color", color)],
+ children=children,
+ )
+
+
+def Div(
+ attributes: list[HTMLAttribute] = [],
+ children: list[HTMLTag] | HTMLTag = [],
+):
+ return Component(tag_name="div", attributes=attributes, children=children)
+
+
+def Icon(
+ name: str,
+ attributes: list[HTMLAttribute] = [],
+):
+ return Component(template=f"cotton/icon/{name}.html", attributes=attributes)
diff --git a/common/utils.py b/common/utils.py
index 8695ebd..160ab21 100644
--- a/common/utils.py
+++ b/common/utils.py
@@ -1,98 +1,7 @@
from datetime import date
-from random import choices
-from string import ascii_lowercase
-from typing import Any, Callable, Generator, TypeVar
+from typing import Any, Generator, TypeVar
-from django.template.loader import render_to_string
-from django.urls import NoReverseMatch, reverse
-from django.utils.safestring import mark_safe
-
-
-def Popover(
- wrapped_content: str,
- popover_content: str = "",
-) -> str:
- id = randomid()
- if popover_content == "":
- popover_content = wrapped_content
- content = f"{wrapped_content}"
- result = mark_safe(
- str(content)
- + render_to_string(
- "cotton/popover.html",
- {
- "id": id,
- "slot": popover_content,
- },
- )
- )
- return result
-
-
-HTMLAttribute = tuple[str, str]
-HTMLTag = str
-
-
-def Component(
- attributes: list[HTMLAttribute] = [],
- children: list[HTMLTag] | HTMLTag = [],
- template: str = "",
- tag_name: str = "",
-) -> HTMLTag:
- if not tag_name and not template:
- raise ValueError("One of template or tag_name is required.")
- if isinstance(children, str):
- children = [children]
- childrenBlob = "\n".join(children)
- attributesList = [f'{name} = "{value}"' for name, value in attributes]
- attributesBlob = " ".join(attributesList)
- tag: str = ""
- if tag_name != "":
- tag = f"{childrenBlob}"
- elif template != "":
- tag = render_to_string(
- template,
- {name: value for name, value in attributes} | {"slot": "\n".join(children)},
- )
- return mark_safe(tag)
-
-
-def A(
- attributes: list[HTMLAttribute] = [],
- children: list[HTMLTag] | HTMLTag = [],
- url: str | Callable[..., Any] = "",
-):
- """
- Returns the HTML tag "a".
- "url" can either be:
- - URL (string)
- - path name passed to reverse() (string)
- - function
- """
- additional_attributes = []
- if url:
- if type(url) is str:
- try:
- url_result = reverse(url)
- except NoReverseMatch:
- url_result = url
- elif callable(url):
- url_result = url()
- else:
- raise TypeError("'url' is neither str nor function.")
- additional_attributes = [("href", url_result)]
- return Component(
- tag_name="a", attributes=attributes + additional_attributes, children=children
- )
-
-
-def Button(
- attributes: list[HTMLAttribute] = [],
- children: list[HTMLTag] | HTMLTag = [],
-):
- return Component(
- template="cotton/button.html", attributes=attributes, children=children
- )
+from common.components import Popover
def safe_division(numerator: int | float, denominator: int | float) -> int | float:
@@ -137,17 +46,11 @@ def truncate(input_string: str, length: int = 30, ellipsis: str = "…") -> str:
def truncate_with_popover(input_string: str) -> str:
if (truncated := truncate(input_string)) != input_string:
- print(f"Not the same after: {truncated=}")
return Popover(wrapped_content=truncated, popover_content=input_string)
else:
- print("Strings are the same!")
return input_string
-def randomid(seed: str = "", length: int = 10) -> str:
- return seed + "".join(choices(ascii_lowercase, k=length))
-
-
T = TypeVar("T", str, int, date)
diff --git a/games/static/base.css b/games/static/base.css
index 906e1c1..c4ad10b 100644
--- a/games/static/base.css
+++ b/games/static/base.css
@@ -1346,6 +1346,10 @@ input:checked + .toggle-bg {
z-index: 50;
}
+.m-4 {
+ margin: 1rem;
+}
+
.mx-2 {
margin-left: 0.5rem;
margin-right: 0.5rem;
@@ -1861,6 +1865,16 @@ input:checked + .toggle-bg {
background-color: rgb(5 122 85 / var(--tw-bg-opacity));
}
+.bg-green-700 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(4 108 78 / var(--tw-bg-opacity));
+}
+
+.bg-red-700 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(200 30 30 / var(--tw-bg-opacity));
+}
+
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
@@ -1941,6 +1955,11 @@ input:checked + .toggle-bg {
padding-bottom: 0.75rem;
}
+.py-3\.5 {
+ padding-top: 0.875rem;
+ padding-bottom: 0.875rem;
+}
+
.py-4 {
padding-top: 1rem;
padding-bottom: 1rem;
@@ -2067,6 +2086,11 @@ input:checked + .toggle-bg {
letter-spacing: -0.025em;
}
+.text-black {
+ --tw-text-opacity: 1;
+ color: rgb(0 0 0 / var(--tw-text-opacity));
+}
+
.text-blue-600 {
--tw-text-opacity: 1;
color: rgb(28 100 242 / var(--tw-text-opacity));
@@ -2519,24 +2543,24 @@ textarea:disabled:is(.dark *) {
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
}
-.hover\:bg-green-500:hover {
- --tw-bg-opacity: 1;
- background-color: rgb(14 159 110 / var(--tw-bg-opacity));
-}
-
.hover\:bg-green-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(4 108 78 / var(--tw-bg-opacity));
}
+.hover\:bg-green-800:hover {
+ --tw-bg-opacity: 1;
+ background-color: rgb(3 84 63 / var(--tw-bg-opacity));
+}
+
.hover\:bg-red-100:hover {
--tw-bg-opacity: 1;
background-color: rgb(253 232 232 / var(--tw-bg-opacity));
}
-.hover\:bg-red-500:hover {
+.hover\:bg-red-800:hover {
--tw-bg-opacity: 1;
- background-color: rgb(240 82 82 / var(--tw-bg-opacity));
+ background-color: rgb(155 28 28 / var(--tw-bg-opacity));
}
.hover\:bg-white:hover {
@@ -2544,6 +2568,16 @@ textarea:disabled:is(.dark *) {
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
+.hover\:bg-green-500:hover {
+ --tw-bg-opacity: 1;
+ background-color: rgb(14 159 110 / var(--tw-bg-opacity));
+}
+
+.hover\:bg-red-500:hover {
+ --tw-bg-opacity: 1;
+ background-color: rgb(240 82 82 / var(--tw-bg-opacity));
+}
+
.hover\:text-blue-600:hover {
--tw-text-opacity: 1;
color: rgb(28 100 242 / var(--tw-text-opacity));
@@ -2610,16 +2644,31 @@ textarea:disabled:is(.dark *) {
--tw-ring-color: rgb(26 86 219 / var(--tw-ring-opacity));
}
+.focus\:ring-gray-100:focus {
+ --tw-ring-opacity: 1;
+ --tw-ring-color: rgb(243 244 246 / var(--tw-ring-opacity));
+}
+
.focus\:ring-gray-200:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(229 231 235 / var(--tw-ring-opacity));
}
+.focus\:ring-green-300:focus {
+ --tw-ring-opacity: 1;
+ --tw-ring-color: rgb(132 225 188 / var(--tw-ring-opacity));
+}
+
.focus\:ring-green-500:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(14 159 110 / var(--tw-ring-opacity));
}
+.focus\:ring-red-300:focus {
+ --tw-ring-opacity: 1;
+ --tw-ring-color: rgb(248 180 180 / var(--tw-ring-opacity));
+}
+
.focus\:ring-green-700:focus {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(4 108 78 / var(--tw-ring-opacity));
@@ -2771,11 +2820,21 @@ textarea:disabled:is(.dark *) {
background-color: rgb(17 24 39 / 0.8);
}
+.dark\:bg-green-600:is(.dark *) {
+ --tw-bg-opacity: 1;
+ background-color: rgb(5 122 85 / var(--tw-bg-opacity));
+}
+
.dark\:bg-purple-800:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(85 33 181 / var(--tw-bg-opacity));
}
+.dark\:bg-red-600:is(.dark *) {
+ --tw-bg-opacity: 1;
+ background-color: rgb(224 36 36 / var(--tw-bg-opacity));
+}
+
.dark\:text-blue-500:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(63 131 248 / var(--tw-text-opacity));
@@ -2861,9 +2920,9 @@ textarea:disabled:is(.dark *) {
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
}
-.dark\:hover\:bg-green-600:hover:is(.dark *) {
+.dark\:hover\:bg-green-700:hover:is(.dark *) {
--tw-bg-opacity: 1;
- background-color: rgb(5 122 85 / var(--tw-bg-opacity));
+ background-color: rgb(4 108 78 / var(--tw-bg-opacity));
}
.dark\:hover\:bg-red-700:hover:is(.dark *) {
@@ -2871,6 +2930,11 @@ textarea:disabled:is(.dark *) {
background-color: rgb(200 30 30 / var(--tw-bg-opacity));
}
+.dark\:hover\:bg-green-600:hover:is(.dark *) {
+ --tw-bg-opacity: 1;
+ background-color: rgb(5 122 85 / var(--tw-bg-opacity));
+}
+
.dark\:hover\:text-blue-500:hover:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(63 131 248 / var(--tw-text-opacity));
@@ -2906,6 +2970,21 @@ textarea:disabled:is(.dark *) {
--tw-ring-color: rgb(75 85 99 / var(--tw-ring-opacity));
}
+.dark\:focus\:ring-gray-700:focus:is(.dark *) {
+ --tw-ring-opacity: 1;
+ --tw-ring-color: rgb(55 65 81 / var(--tw-ring-opacity));
+}
+
+.dark\:focus\:ring-green-800:focus:is(.dark *) {
+ --tw-ring-opacity: 1;
+ --tw-ring-color: rgb(3 84 63 / var(--tw-ring-opacity));
+}
+
+.dark\:focus\:ring-red-900:focus:is(.dark *) {
+ --tw-ring-opacity: 1;
+ --tw-ring-color: rgb(119 29 29 / var(--tw-ring-opacity));
+}
+
.dark\:focus\:ring-green-500:focus:is(.dark *) {
--tw-ring-opacity: 1;
--tw-ring-color: rgb(14 159 110 / var(--tw-ring-opacity));
@@ -3120,3 +3199,7 @@ textarea:disabled:is(.dark *) {
.\[\&_a\]\:underline-offset-4 a {
text-underline-offset: 4px;
}
+
+.\[\&_h1\]\:mb-2 h1 {
+ margin-bottom: 0.5rem;
+}
diff --git a/games/templates/cotton/button.html b/games/templates/cotton/button.html
index 3cc1f1f..22131de 100644
--- a/games/templates/cotton/button.html
+++ b/games/templates/cotton/button.html
@@ -1,5 +1,6 @@
+