Unify disabled appearance across all form controls

Disabled controls looked inconsistent: the SearchSelect faded (opacity-50)
while native inputs used a solid strong surface. Standardize on the faded look
(opacity-50) the user preferred, via shared constants so every form element
matches:

- DISABLED_CONTROL_CLASS (disabled:opacity-50 disabled:cursor-not-allowed) on
  the control — native inputs/select/textarea via PrimitiveWidgetsMixin, plus
  the Checkbox component (previously had no disabled style).
- DISABLED_WITHIN_CLASS (has-[:disabled]: wrapper variant) for composite
  controls like SearchSelect whose disabled state lives on an inner element.

e2e asserts a disabled SearchSelect and the Name input fade identically
(opacity 0.5) and return to 1 when enabled. CLAUDE.md documents the shared
disabled constants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 07:52:44 +02:00
parent 02798f8858
commit 29ba3e66e9
7 changed files with 50 additions and 34 deletions
+4
View File
@@ -53,6 +53,8 @@ from common.components.primitives import (
A,
AddForm,
ButtonGroup,
DISABLED_CONTROL_CLASS,
DISABLED_WITHIN_CLASS,
FormFields,
Checkbox,
CsrfInput,
@@ -115,6 +117,8 @@ __all__ = [
"randomid",
"A",
"AddForm",
"DISABLED_CONTROL_CLASS",
"DISABLED_WITHIN_CLASS",
"FormFields",
"StyledButton",
"ButtonGroup",
+9 -1
View File
@@ -46,6 +46,13 @@ _SIZE_CLASSES = {
"xl": "px-6 py-3.5 text-base",
}
# Shared disabled appearance for every form control, so all form elements look
# the same when disabled. Put on the control itself (DISABLED_CONTROL_CLASS) or,
# for composite controls whose disabled state lives on an inner element (e.g.
# SearchSelect), on the wrapper via :has() (DISABLED_WITHIN_CLASS).
DISABLED_CONTROL_CLASS = "disabled:opacity-50 disabled:cursor-not-allowed"
DISABLED_WITHIN_CLASS = "has-[:disabled]:opacity-50 has-[:disabled]:cursor-not-allowed"
# ── Generic leaf elements ────────────────────────────────────────────────────
# A whitelist of plain tags, each turned into a builder over `Element`. The
@@ -404,7 +411,8 @@ def Checkbox(
("value", value),
(
"class",
"rounded border-default-medium bg-neutral-secondary-medium text-brand focus:ring-brand",
"rounded border-default-medium bg-neutral-secondary-medium "
f"text-brand focus:ring-brand {DISABLED_CONTROL_CLASS}",
),
] + attributes
if checked:
+15 -7
View File
@@ -34,7 +34,14 @@ from typing import TypedDict
from common.components.core import Attributes, Element, HTMLAttribute, Media, Node
from common.components.primitives import Div, Input, Pill, Span, Template
from common.components.primitives import (
DISABLED_WITHIN_CLASS,
Div,
Input,
Pill,
Span,
Template,
)
# Both comboboxes are wired by ts/search_select.ts (compiled to dist/).
_SEARCH_SELECT_MEDIA = Media(js=("dist/search_select.js",))
@@ -58,17 +65,18 @@ LabeledOption = tuple[str, str]
# with the search input. The options panel is absolute, so it sits outside the
# flex flow. (border omitted intentionally — see if it's needed later.)
# The widget owns its disabled appearance: when any control inside it is
# :disabled (e.g. add_purchase.ts disabling the search input), the wrapper greys
# itself via :has() — callers only toggle the control's `disabled`, never styles.
# :disabled (e.g. add_purchase.ts disabling the search input), the wrapper fades
# via :has() — the same opacity-50 a disabled native input uses (see
# _DISABLED_CONTROL in games/forms.py), so the two look identical. Callers only
# toggle the control's `disabled`, never styles.
_CONTAINER_CLASS = (
"relative flex flex-wrap items-center gap-1 p-2 "
"rounded-base bg-neutral-secondary-medium "
"has-[:disabled]:opacity-50 has-[:disabled]:cursor-not-allowed"
f"rounded-base bg-neutral-secondary-medium {DISABLED_WITHIN_CLASS}"
)
_PILLS_CLASS = "contents"
# disabled:cursor-not-allowed matches the wrapper's cursor so hovering across
# the whole widget stays consistent the inner input is excluded from the
# global disabled rule (input.css), which would otherwise have set it.
# the whole widget stays consistent (the wrapper handles the faded look via
# has-[:disabled]:opacity-50).
_SEARCH_CLASS = (
"flex-1 min-w-[8rem] border-0 bg-transparent text-sm text-heading "
"focus:ring-0 focus:outline-hidden placeholder:text-body "