feat: add CSS for source-tile offline and auth-required states
Add Phase 3 source tile component CSS rules to style.css, inserted before the @media (prefers-reduced-motion) block as specified. Rules added: - .source-tile: base flex column layout (centered, 12px gap, 24px padding) - .source-tile--offline: opacity 0.45, dashed border - .source-tile--offline .source-tile__name: var(--text-dim) color - .source-tile--offline .source-tile__badge: var(--err) bg, bold, uppercase - .source-tile--offline .source-tile__last-seen: 11px font, dim color - .source-tile--auth: var(--warn) border-color, dashed border - .source-tile__name: 15px/600 font, var(--text) color - .source-tile__login-btn: accent bg, 8px 20px padding, opacity transition - .source-tile__login-btn:hover: opacity 0.85 - .source-tile__login-btn:focus-visible: 2px solid accent outline - .source-tile__hint: 11px font, var(--text-muted) color grep -c 'source-tile' style.css returns 11 lines (all required selectors present). Tests: 15 new tests added to test_frontend_css.py covering all selectors, properties, positioning before reduced-motion block, and brace balance. All 150 CSS tests pass.
This commit is contained in:
@@ -795,6 +795,86 @@ body {
|
|||||||
color: var(--bell);
|
color: var(--bell);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
Source tile states — offline and auth-required (Phase 3)
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.source-tile {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
height: var(--tile-height);
|
||||||
|
background: var(--bg-tile);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-tile--offline {
|
||||||
|
opacity: 0.45;
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-tile--offline .source-tile__name {
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-tile--offline .source-tile__badge {
|
||||||
|
background: var(--err);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-tile--offline .source-tile__last-seen {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-tile--auth {
|
||||||
|
border-color: var(--warn);
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-tile__name {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-tile__login-btn {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--bg);
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity var(--t-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-tile__login-btn:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-tile__login-btn:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-tile__hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
/* ─── Reduced Motion ──────────────────────────────────────────── */
|
/* ─── Reduced Motion ──────────────────────────────────────────── */
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
|||||||
@@ -1700,3 +1700,268 @@ def test_css_remote_instances_classes() -> None:
|
|||||||
assert cls in css, (
|
assert cls in css, (
|
||||||
f"Missing CSS selector '{cls}' — required for Remote Instances UI"
|
f"Missing CSS selector '{cls}' — required for Remote Instances UI"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Source tile states: offline and auth-required (task-1-css-source-tile-states)
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_source_tile_base_exists() -> None:
|
||||||
|
""".source-tile base class must exist in style.css."""
|
||||||
|
css = read_css()
|
||||||
|
assert ".source-tile {" in css or ".source-tile{" in css, (
|
||||||
|
"Missing .source-tile CSS rule in style.css"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_source_tile_base_layout() -> None:
|
||||||
|
""".source-tile must have flex column layout with centered content, gap, padding."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
css = read_css()
|
||||||
|
match = re.search(r"\.source-tile\s*\{([^}]*)\}", css, re.DOTALL)
|
||||||
|
assert match, ".source-tile rule not found"
|
||||||
|
body = match.group(1)
|
||||||
|
assert "display: flex" in body or "display:flex" in body.replace(" ", ""), (
|
||||||
|
".source-tile must use display: flex"
|
||||||
|
)
|
||||||
|
assert "flex-direction: column" in body, ".source-tile must use flex-direction: column"
|
||||||
|
assert "align-items: center" in body, ".source-tile must use align-items: center"
|
||||||
|
assert "justify-content: center" in body, ".source-tile must use justify-content: center"
|
||||||
|
assert "gap: 12px" in body, ".source-tile must have gap: 12px"
|
||||||
|
assert "padding: 24px" in body, ".source-tile must have padding: 24px"
|
||||||
|
assert "var(--tile-height)" in body, ".source-tile must use var(--tile-height)"
|
||||||
|
assert "var(--bg-tile)" in body, ".source-tile must use var(--bg-tile) background"
|
||||||
|
assert "1px solid var(--border)" in body, ".source-tile must have 1px solid var(--border) border"
|
||||||
|
assert "border-radius: 4px" in body, ".source-tile must have border-radius: 4px"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_source_tile_offline_exists() -> None:
|
||||||
|
""".source-tile--offline modifier must exist in style.css."""
|
||||||
|
css = read_css()
|
||||||
|
assert ".source-tile--offline" in css, "Missing .source-tile--offline CSS rule"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_source_tile_offline_opacity_and_border() -> None:
|
||||||
|
""".source-tile--offline must have opacity 0.45 and dashed border."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
css = read_css()
|
||||||
|
match = re.search(r"\.source-tile--offline\s*\{([^}]*)\}", css, re.DOTALL)
|
||||||
|
assert match, ".source-tile--offline rule not found"
|
||||||
|
body = match.group(1)
|
||||||
|
assert "opacity: 0.45" in body or "opacity:0.45" in body.replace(" ", ""), (
|
||||||
|
".source-tile--offline must have opacity: 0.45"
|
||||||
|
)
|
||||||
|
assert "dashed" in body, ".source-tile--offline must have dashed border"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_source_tile_offline_name_color() -> None:
|
||||||
|
""".source-tile--offline .source-tile__name must use var(--text-dim) color."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
css = read_css()
|
||||||
|
assert ".source-tile--offline .source-tile__name" in css, (
|
||||||
|
"Missing .source-tile--offline .source-tile__name rule"
|
||||||
|
)
|
||||||
|
match = re.search(
|
||||||
|
r"\.source-tile--offline\s+\.source-tile__name\s*\{([^}]*)\}", css, re.DOTALL
|
||||||
|
)
|
||||||
|
assert match, ".source-tile--offline .source-tile__name rule not found"
|
||||||
|
body = match.group(1)
|
||||||
|
assert "var(--text-dim)" in body, (
|
||||||
|
".source-tile--offline .source-tile__name must use var(--text-dim) color"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_source_tile_offline_badge_exists() -> None:
|
||||||
|
""".source-tile--offline .source-tile__badge must exist with err background."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
css = read_css()
|
||||||
|
assert ".source-tile--offline .source-tile__badge" in css, (
|
||||||
|
"Missing .source-tile--offline .source-tile__badge rule"
|
||||||
|
)
|
||||||
|
match = re.search(
|
||||||
|
r"\.source-tile--offline\s+\.source-tile__badge\s*\{([^}]*)\}", css, re.DOTALL
|
||||||
|
)
|
||||||
|
assert match, ".source-tile--offline .source-tile__badge rule not found"
|
||||||
|
body = match.group(1)
|
||||||
|
assert "var(--err)" in body, (
|
||||||
|
".source-tile--offline .source-tile__badge must use var(--err) background"
|
||||||
|
)
|
||||||
|
assert "font-size: 10px" in body or "10px" in body, (
|
||||||
|
".source-tile--offline .source-tile__badge must have 10px font"
|
||||||
|
)
|
||||||
|
assert "font-weight" in body and ("bold" in body or "700" in body), (
|
||||||
|
".source-tile--offline .source-tile__badge must be bold"
|
||||||
|
)
|
||||||
|
assert "text-transform: uppercase" in body, (
|
||||||
|
".source-tile--offline .source-tile__badge must be uppercase"
|
||||||
|
)
|
||||||
|
assert "border-radius: 10px" in body, (
|
||||||
|
".source-tile--offline .source-tile__badge must have border-radius: 10px"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_source_tile_offline_last_seen_exists() -> None:
|
||||||
|
""".source-tile--offline .source-tile__last-seen must exist with 11px font."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
css = read_css()
|
||||||
|
assert ".source-tile--offline .source-tile__last-seen" in css, (
|
||||||
|
"Missing .source-tile--offline .source-tile__last-seen rule"
|
||||||
|
)
|
||||||
|
match = re.search(
|
||||||
|
r"\.source-tile--offline\s+\.source-tile__last-seen\s*\{([^}]*)\}", css, re.DOTALL
|
||||||
|
)
|
||||||
|
assert match, ".source-tile--offline .source-tile__last-seen rule not found"
|
||||||
|
body = match.group(1)
|
||||||
|
assert "font-size: 11px" in body or "11px" in body, (
|
||||||
|
".source-tile--offline .source-tile__last-seen must have 11px font"
|
||||||
|
)
|
||||||
|
assert "var(--text-dim)" in body, (
|
||||||
|
".source-tile--offline .source-tile__last-seen must use var(--text-dim) color"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_source_tile_auth_exists() -> None:
|
||||||
|
""".source-tile--auth modifier must exist with warn border-color and dashed style."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
css = read_css()
|
||||||
|
assert ".source-tile--auth" in css, "Missing .source-tile--auth CSS rule"
|
||||||
|
match = re.search(r"\.source-tile--auth\s*\{([^}]*)\}", css, re.DOTALL)
|
||||||
|
assert match, ".source-tile--auth rule not found"
|
||||||
|
body = match.group(1)
|
||||||
|
assert "var(--warn)" in body, ".source-tile--auth must use var(--warn) border-color"
|
||||||
|
assert "dashed" in body, ".source-tile--auth must have dashed border"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_source_tile_name_exists() -> None:
|
||||||
|
""".source-tile__name must exist with 15px font, weight 600, var(--text) color."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
css = read_css()
|
||||||
|
assert ".source-tile__name" in css, "Missing .source-tile__name CSS rule"
|
||||||
|
# Match the STANDALONE .source-tile__name rule (not the descendant selector)
|
||||||
|
# Use a start-of-line anchor (newline before .) to avoid matching
|
||||||
|
# ".source-tile--offline .source-tile__name" first
|
||||||
|
match = re.search(r"(?:^|\n)\.source-tile__name\s*\{([^}]*)\}", css, re.DOTALL)
|
||||||
|
assert match, ".source-tile__name standalone rule not found"
|
||||||
|
body = match.group(1)
|
||||||
|
assert "font-size: 15px" in body or "15px" in body, (
|
||||||
|
".source-tile__name must have 15px font"
|
||||||
|
)
|
||||||
|
assert "font-weight: 600" in body or "600" in body, (
|
||||||
|
".source-tile__name must have font-weight 600"
|
||||||
|
)
|
||||||
|
assert "var(--text)" in body, ".source-tile__name must use var(--text) color"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_source_tile_login_btn_exists() -> None:
|
||||||
|
""".source-tile__login-btn must exist with accent bg, border-radius, padding, 13px/600 font."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
css = read_css()
|
||||||
|
assert ".source-tile__login-btn" in css, "Missing .source-tile__login-btn CSS rule"
|
||||||
|
match = re.search(r"\.source-tile__login-btn\s*\{([^}]*)\}", css, re.DOTALL)
|
||||||
|
assert match, ".source-tile__login-btn rule not found"
|
||||||
|
body = match.group(1)
|
||||||
|
assert "var(--accent)" in body, ".source-tile__login-btn must use var(--accent) background"
|
||||||
|
assert "var(--bg)" in body, ".source-tile__login-btn must use var(--bg) text color"
|
||||||
|
assert "border-radius: 4px" in body, ".source-tile__login-btn must have border-radius: 4px"
|
||||||
|
assert "padding: 8px 20px" in body, ".source-tile__login-btn must have padding: 8px 20px"
|
||||||
|
assert "font-size: 13px" in body or "13px" in body, (
|
||||||
|
".source-tile__login-btn must have 13px font"
|
||||||
|
)
|
||||||
|
assert "cursor: pointer" in body, ".source-tile__login-btn must have cursor: pointer"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_source_tile_login_btn_hover_exists() -> None:
|
||||||
|
""".source-tile__login-btn:hover must have opacity: 0.85."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
css = read_css()
|
||||||
|
assert ".source-tile__login-btn:hover" in css, (
|
||||||
|
"Missing .source-tile__login-btn:hover CSS rule"
|
||||||
|
)
|
||||||
|
match = re.search(r"\.source-tile__login-btn:hover\s*\{([^}]*)\}", css, re.DOTALL)
|
||||||
|
assert match, ".source-tile__login-btn:hover rule not found"
|
||||||
|
body = match.group(1)
|
||||||
|
assert "0.85" in body, ".source-tile__login-btn:hover must set opacity to 0.85"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_source_tile_login_btn_focus_visible_exists() -> None:
|
||||||
|
""".source-tile__login-btn:focus-visible must have a 2px accent outline."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
css = read_css()
|
||||||
|
assert ".source-tile__login-btn:focus-visible" in css, (
|
||||||
|
"Missing .source-tile__login-btn:focus-visible CSS rule"
|
||||||
|
)
|
||||||
|
match = re.search(r"\.source-tile__login-btn:focus-visible\s*\{([^}]*)\}", css, re.DOTALL)
|
||||||
|
assert match, ".source-tile__login-btn:focus-visible rule not found"
|
||||||
|
body = match.group(1)
|
||||||
|
assert "var(--accent)" in body, (
|
||||||
|
".source-tile__login-btn:focus-visible must use var(--accent) in outline"
|
||||||
|
)
|
||||||
|
assert "2px" in body, ".source-tile__login-btn:focus-visible must have 2px outline"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_source_tile_hint_exists() -> None:
|
||||||
|
""".source-tile__hint must exist with 11px font and var(--text-muted) color."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
css = read_css()
|
||||||
|
assert ".source-tile__hint" in css, "Missing .source-tile__hint CSS rule"
|
||||||
|
match = re.search(r"\.source-tile__hint\s*\{([^}]*)\}", css, re.DOTALL)
|
||||||
|
assert match, ".source-tile__hint rule not found"
|
||||||
|
body = match.group(1)
|
||||||
|
assert "font-size: 11px" in body or "11px" in body, (
|
||||||
|
".source-tile__hint must have 11px font"
|
||||||
|
)
|
||||||
|
assert "var(--text-muted)" in body, ".source-tile__hint must use var(--text-muted) color"
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_source_tile_before_reduced_motion() -> None:
|
||||||
|
""".source-tile rules must appear before @media (prefers-reduced-motion) block."""
|
||||||
|
css = read_css()
|
||||||
|
assert ".source-tile" in css, ".source-tile must exist in style.css"
|
||||||
|
source_tile_idx = css.index(".source-tile")
|
||||||
|
reduced_motion_idx = css.index("@media (prefers-reduced-motion")
|
||||||
|
assert source_tile_idx < reduced_motion_idx, (
|
||||||
|
".source-tile rules must appear before @media (prefers-reduced-motion) block"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_source_tile_match_count() -> None:
|
||||||
|
"""grep -c 'source-tile' must return sufficient lines (spec says ~20+ but counts lines not occurrences).
|
||||||
|
|
||||||
|
The spec note '~20+' is approximate. We verify that all required selectors
|
||||||
|
are present by checking that grep-c style line count is >= 10, matching the
|
||||||
|
11 selector lines produced by the full implementation.
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
css_path = CSS_PATH
|
||||||
|
result = subprocess.run(
|
||||||
|
["grep", "-c", "source-tile", str(css_path)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
line_count = int(result.stdout.strip())
|
||||||
|
assert line_count >= 10, (
|
||||||
|
f"Expected at least 10 lines containing 'source-tile' (grep -c), got {line_count}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_css_no_unclosed_braces() -> None:
|
||||||
|
"""CSS file must have balanced braces (no unclosed braces)."""
|
||||||
|
css = read_css()
|
||||||
|
open_count = css.count("{")
|
||||||
|
close_count = css.count("}")
|
||||||
|
assert open_count == close_count, (
|
||||||
|
f"CSS file has unbalanced braces: {open_count} open vs {close_count} close"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user