diff --git a/common/components/__init__.py b/common/components/__init__.py index 0763b1f..174f8db 100644 --- a/common/components/__init__.py +++ b/common/components/__init__.py @@ -36,6 +36,7 @@ from common.components.primitives import ( AddForm, Button, ButtonGroup, + Checkbox, CsrfInput, Div, ExternalScript, @@ -48,6 +49,7 @@ from common.components.primitives import ( Pill, Popover, PopoverTruncated, + Radio, SearchField, SimpleTable, Span, @@ -82,6 +84,7 @@ __all__ = [ "AddForm", "Button", "ButtonGroup", + "Checkbox", "CsrfInput", "Div", "ExternalScript", @@ -93,6 +96,7 @@ __all__ = [ "Pill", "Popover", "PopoverTruncated", + "Radio", "SearchField", "DEFAULT_PREFETCH", "FilterSelect", diff --git a/common/components/primitives.py b/common/components/primitives.py index f88bd1f..11a5eab 100644 --- a/common/components/primitives.py +++ b/common/components/primitives.py @@ -401,6 +401,68 @@ def Label( return Component(tag_name="label", attributes=attributes, children=children) +def Checkbox( + name: str, + label: str, + checked: bool = False, + value: str = "1", + attributes: list[HTMLAttribute] | None = None, +) -> SafeText: + """A filter-agnostic Checkbox component.""" + attributes = attributes or [] + input_attrs = [ + ("name", name), + ("value", value), + ( + "class", + "rounded border-default-medium bg-neutral-secondary-medium text-brand focus:ring-brand", + ), + ] + attributes + if checked: + input_attrs.append(("checked", "true")) + + return Label( + attributes=[ + ("class", "flex items-center gap-2 text-sm text-heading cursor-pointer") + ], + children=[ + Input(type="checkbox", attributes=input_attrs), + label, + ], + ) + + +def Radio( + name: str, + label: str, + checked: bool = False, + value: str = "", + attributes: list[HTMLAttribute] | None = None, +) -> SafeText: + """A filter-agnostic Radio component.""" + attributes = attributes or [] + input_attrs = [ + ("name", name), + ("value", value), + ( + "class", + "rounded-full border-default-medium bg-neutral-secondary-medium text-brand focus:ring-brand", + ), + ] + attributes + if checked: + input_attrs.append(("checked", "true")) + + return Label( + attributes=[ + ("class", "flex items-center gap-1.5 text-sm text-heading cursor-pointer") + ], + children=[ + Input(type="radio", attributes=input_attrs), + label, + ], + ) + + def Template( attributes: list[HTMLAttribute] | None = None, children: list[HTMLTag] | HTMLTag | None = None, diff --git a/games/static/base.css b/games/static/base.css index c24c3e6..1383d33 100644 --- a/games/static/base.css +++ b/games/static/base.css @@ -293,27 +293,85 @@ --leading-5: 20px; --radius-base: 12px; --color-body: var(--color-gray-600); + --color-body-subtle: var(--color-gray-500); --color-heading: var(--color-gray-900); + --color-fg-brand-subtle: var(--color-blue-200); --color-fg-brand: var(--color-blue-700); + --color-fg-brand-strong: var(--color-blue-900); + --color-fg-success: var(--color-emerald-700); + --color-fg-success-strong: var(--color-emerald-900); + --color-fg-danger: var(--color-rose-700); + --color-fg-danger-strong: var(--color-rose-900); + --color-fg-warning-subtle: var(--color-orange-600); + --color-fg-warning: var(--color-orange-900); + --color-fg-yellow: var(--color-yellow-400); --color-fg-disabled: var(--color-gray-400); + --color-fg-purple: var(--color-purple-600); + --color-fg-cyan: var(--color-cyan-600); + --color-fg-indigo: var(--color-indigo-600); + --color-fg-pink: var(--color-pink-600); + --color-fg-lime: var(--color-lime-600); --color-neutral-primary-soft: var(--color-white); --color-neutral-primary: var(--color-white); --color-neutral-primary-medium: var(--color-white); + --color-neutral-primary-strong: var(--color-white); --color-neutral-secondary-soft: var(--color-gray-50); --color-neutral-secondary: var(--color-gray-50); --color-neutral-secondary-medium: var(--color-gray-50); --color-neutral-secondary-strong: var(--color-gray-50); + --color-neutral-secondary-strongest: var(--color-gray-50); + --color-neutral-tertiary-soft: var(--color-gray-100); --color-neutral-tertiary: var(--color-gray-100); --color-neutral-tertiary-medium: var(--color-gray-100); --color-neutral-quaternary: var(--color-gray-200); + --color-neutral-quaternary-medium: var(--color-gray-200); + --color-gray: var(--color-gray-300); + --color-brand-softer: var(--color-blue-50); --color-brand-soft: var(--color-blue-100); --color-brand: var(--color-blue-700); --color-brand-medium: var(--color-blue-200); --color-brand-strong: var(--color-blue-800); + --color-success-soft: var(--color-emerald-50); + --color-success: var(--color-emerald-700); + --color-success-medium: var(--color-emerald-100); + --color-success-strong: var(--color-emerald-800); + --color-danger-soft: var(--color-rose-50); + --color-danger: var(--color-rose-700); + --color-danger-medium: var(--color-rose-100); + --color-danger-strong: var(--color-rose-800); + --color-warning-soft: var(--color-orange-50); + --color-warning: var(--color-orange-500); + --color-warning-medium: var(--color-orange-100); + --color-warning-strong: var(--color-orange-700); + --color-dark-soft: var(--color-gray-800); --color-dark: var(--color-gray-800); + --color-dark-strong: var(--color-gray-900); + --color-disabled: var(--color-gray-100); + --color-purple: var(--color-purple-500); + --color-sky: var(--color-sky-500); + --color-teal: var(--color-teal-600); + --color-pink: var(--color-pink-600); + --color-cyan: var(--color-cyan-500); + --color-fuchsia: var(--color-fuchsia-600); + --color-indigo: var(--color-indigo-600); + --color-orange: var(--color-orange-400); + --color-buffer: var(--color-white); + --color-buffer-medium: var(--color-white); + --color-buffer-strong: var(--color-white); + --color-muted: var(--color-gray-50); + --color-light-subtle: var(--color-gray-100); --color-light: var(--color-gray-100); + --color-light-medium: var(--color-gray-100); + --color-default-subtle: var(--color-gray-200); --color-default: var(--color-gray-200); --color-default-medium: var(--color-gray-200); + --color-default-strong: var(--color-gray-200); + --color-success-subtle: var(--color-emerald-200); + --color-danger-subtle: var(--color-rose-200); + --color-warning-subtle: var(--color-orange-200); + --color-brand-subtle: var(--color-blue-200); + --color-brand-light: var(--color-blue-600); + --color-dark-subtle: var(--color-gray-800); --color-dark-backdrop: var(--color-gray-950); --color-accent: #7c3aed; } @@ -823,12 +881,18 @@ .start-0 { inset-inline-start: calc(var(--spacing) * 0); } + .end-1 { + inset-inline-end: calc(var(--spacing) * 1); + } .end-1\.5 { inset-inline-end: calc(var(--spacing) * 1.5); } .top-0 { top: calc(var(--spacing) * 0); } + .top-1 { + top: calc(var(--spacing) * 1); + } .top-1\/2 { top: calc(1 / 2 * 100%); } @@ -850,6 +914,9 @@ .bottom-0 { bottom: calc(var(--spacing) * 0); } + .bottom-1 { + bottom: calc(var(--spacing) * 1); + } .bottom-1\.5 { bottom: calc(var(--spacing) * 1.5); } @@ -1476,6 +1543,9 @@ .h-8 { height: calc(var(--spacing) * 8); } + .h-9 { + height: calc(var(--spacing) * 9); + } .h-10 { height: calc(var(--spacing) * 10); } @@ -1559,9 +1629,15 @@ text-align: center; } } + .w-1 { + width: calc(var(--spacing) * 1); + } .w-1\/2 { width: calc(1 / 2 * 100%); } + .w-2 { + width: calc(var(--spacing) * 2); + } .w-2\.5 { width: calc(var(--spacing) * 2.5); } @@ -1679,6 +1755,9 @@ .shrink-0 { flex-shrink: 0; } + .border-collapse { + border-collapse: collapse; + } .-translate-x-full { --tw-translate-x: -100%; translate: var(--tw-translate-x) var(--tw-translate-y); @@ -1695,6 +1774,10 @@ --tw-translate-x: 100%; translate: var(--tw-translate-x) var(--tw-translate-y); } + .-translate-y-1 { + --tw-translate-y: calc(var(--spacing) * -1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } .-translate-y-1\/2 { --tw-translate-y: calc(calc(1 / 2 * 100%) * -1); translate: var(--tw-translate-x) var(--tw-translate-y); @@ -1779,6 +1862,9 @@ .gap-1 { gap: calc(var(--spacing) * 1); } + .gap-1\.5 { + gap: calc(var(--spacing) * 1.5); + } .gap-2 { gap: calc(var(--spacing) * 2); } @@ -1791,6 +1877,9 @@ .gap-5 { gap: calc(var(--spacing) * 5); } + .gap-6 { + gap: calc(var(--spacing) * 6); + } .space-y-6 { :where(& > :not(:last-child)) { --tw-space-y-reverse: 0; @@ -2080,12 +2169,18 @@ .bg-amber-50 { background-color: var(--color-amber-50); } + .bg-amber-500 { + background-color: var(--color-amber-500); + } .bg-amber-500\/15 { background-color: color-mix(in srgb, oklch(76.9% 0.188 70.08) 15%, transparent); @supports (color: color-mix(in lab, red, red)) { background-color: color-mix(in oklab, var(--color-amber-500) 15%, transparent); } } + .bg-black { + background-color: var(--color-black); + } .bg-black\/70 { background-color: color-mix(in srgb, #000 70%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -2110,6 +2205,9 @@ background-color: color-mix(in oklab, var(--color-brand) 15%, transparent); } } + .bg-dark-backdrop { + background-color: var(--color-dark-backdrop); + } .bg-dark-backdrop\/70 { background-color: color-mix(in srgb, oklch(13% 0.028 261.692) 70%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -2128,12 +2226,18 @@ .bg-gray-500 { background-color: var(--color-gray-500); } + .bg-gray-800 { + background-color: var(--color-gray-800); + } .bg-gray-800\/20 { background-color: color-mix(in srgb, oklch(27.8% 0.033 256.848) 20%, transparent); @supports (color: color-mix(in lab, red, red)) { background-color: color-mix(in oklab, var(--color-gray-800) 20%, transparent); } } + .bg-gray-900 { + background-color: var(--color-gray-900); + } .bg-gray-900\/50 { background-color: color-mix(in srgb, oklch(21% 0.034 264.665) 50%, transparent); @supports (color: color-mix(in lab, red, red)) { @@ -2263,6 +2367,18 @@ fill: white !important; } } + .apexcharts-gridline { + stroke: var(--color-default) !important; + .dark & { + stroke: var(--color-default) !important; + } + } + .apexcharts-xcrosshairs { + stroke: var(--color-default) !important; + .dark & { + stroke: var(--color-default) !important; + } + } .apexcharts-ycrosshairs { stroke: var(--color-default) !important; .dark & { @@ -2321,6 +2437,9 @@ .px-6 { padding-inline: calc(var(--spacing) * 6); } + .py-0 { + padding-block: calc(var(--spacing) * 0); + } .py-0\.5 { padding-block: calc(var(--spacing) * 0.5); } @@ -2547,6 +2666,9 @@ .text-balance { text-wrap: balance; } + .text-wrap { + text-wrap: wrap; + } .whitespace-nowrap { white-space: nowrap; } @@ -2682,6 +2804,9 @@ .line-through { text-decoration-line: line-through; } + .no-underline { + text-decoration-line: none; + } .no-underline\! { text-decoration-line: none !important; } @@ -2748,6 +2873,10 @@ -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); } + .backdrop-filter { + -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + } .transition { transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events; transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); diff --git a/tests/test_components.py b/tests/test_components.py index 81f2923..8af0690 100644 --- a/tests/test_components.py +++ b/tests/test_components.py @@ -821,5 +821,27 @@ class SimpleTableRenderingTest(unittest.TestCase): self.assertIn("2025-01-01", tbody) +from django.test import SimpleTestCase +from common.components.primitives import Checkbox, Radio + + +class ComponentPrimitivesTest(SimpleTestCase): + def test_checkbox_primitive(self): + html = Checkbox(name="test-check", label="Accept Terms", checked=True, value="yes") + self.assertIn('type="checkbox"', html) + self.assertIn('name="test-check"', html) + self.assertIn('value="yes"', html) + self.assertIn('checked="true"', html) + self.assertIn("Accept Terms", html) + + def test_radio_primitive(self): + html = Radio(name="test-radio", label="Option A", checked=False, value="A") + self.assertIn('type="radio"', html) + self.assertIn('name="test-radio"', html) + self.assertIn('value="A"', html) + self.assertNotIn('checked="true"', html) + self.assertIn("Option A", html) + + if __name__ == "__main__": unittest.main()