diff --git a/backend/bot/app/web/themes/ascii/style.css b/backend/bot/app/web/themes/ascii/style.css index 1d49250..3d21c78 100644 --- a/backend/bot/app/web/themes/ascii/style.css +++ b/backend/bot/app/web/themes/ascii/style.css @@ -318,6 +318,70 @@ box-shadow: inset 0 0 0 1px #ffffff; } +/* ---------- Admin controls: range sliders and sortable rows ---------- */ + +.theme-key-ascii .ui-range-input { + height: 20px; +} + +.theme-key-ascii .ui-range-input::before { + height: 8px; + border: 1px solid #ffffff; + background: #000000; +} + +.theme-key-ascii .ui-range-input__range { + height: 8px; + background: #ffffff; +} + +.theme-key-ascii .ui-range-input__thumb { + width: 16px; + height: 18px; + border: 1px solid #ffffff; + border-radius: 0; + background: #000000; + box-shadow: none; + transition: none; +} + +.theme-key-ascii .ui-range-input__thumb:hover, +.theme-key-ascii .ui-range-input__thumb:focus-visible { + background: #ffffff; + color: #000000; + box-shadow: 0 0 0 1px #000000; +} + +.theme-key-ascii .ui-sortable { + gap: 6px; +} + +.theme-key-ascii .ui-sortable-item.is-dragging { + opacity: 0.72; +} + +.theme-key-ascii .ui-sortable-item.is-drop-target { + outline: 1px dashed #ffffff; + outline-offset: 2px; + background: rgba(255, 255, 255, 0.08); +} + +.theme-key-ascii .ui-sortable-handle { + align-self: center; + height: 28px; + border: 1px solid #ffffff; + background: #000000; + color: #ffffff; + box-shadow: none; +} + +.theme-key-ascii .ui-sortable-handle:hover, +.theme-key-ascii .ui-sortable-handle:focus-visible, +.theme-key-ascii .ui-sortable-handle:active { + background: #ffffff; + color: #000000; +} + /* ---------- New webapp surfaces: support, purchase info, password login ---------- */ .theme-key-ascii .trial-offer-card, @@ -1104,6 +1168,7 @@ body:has(.theme-key-ascii) .install-platform-item[data-selected] svg { .admin-cn-card-skeleton--tall, .admin-input, .admin-textarea, .admin-btn, .admin-chip, .admin-tabs-trigger, .admin-tabs-list, + .ui-range-input__thumb, .ui-sortable-item, .ui-sortable-handle, .admin-nav-item, .admin-revenue-period-btn, .admin-mobile-toggle, .admin-header, .admin-sidebar, .admin-sidebar-brand, .admin-dialog, diff --git a/backend/bot/app/web/themes/ascii/theme.json b/backend/bot/app/web/themes/ascii/theme.json index 25f6a12..9ab9e61 100644 --- a/backend/bot/app/web/themes/ascii/theme.json +++ b/backend/bot/app/web/themes/ascii/theme.json @@ -9,7 +9,7 @@ "use_primary_accent": false, "use_in_admin": true, "css_file": "style.css", - "assets_version": 5, + "assets_version": 6, "tokens": { "color_scheme": "dark", "style_preset": "ascii" diff --git a/backend/bot/app/web/themes/light/style.css b/backend/bot/app/web/themes/light/style.css index af378f1..213309c 100644 --- a/backend/bot/app/web/themes/light/style.css +++ b/backend/bot/app/web/themes/light/style.css @@ -205,6 +205,42 @@ body:has(.theme-key-light) .install-platform-item[data-selected] { color: color-mix(in srgb, var(--accent) 55%, #000000); } +/* Admin controls: range sliders and sortable rows */ + +.theme-key-light .ui-range-input::before { + background: rgba(15, 23, 42, 0.12); +} + +.theme-key-light .ui-range-input__range { + background: color-mix(in srgb, var(--accent) 70%, #0f172a); +} + +.theme-key-light .ui-range-input__thumb { + border-color: color-mix(in srgb, var(--accent) 68%, #0f172a); + background: #ffffff; + box-shadow: 0 2px 8px rgba(15, 23, 42, 0.18); +} + +.theme-key-light .ui-range-input__thumb:focus-visible { + box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 18%, transparent); +} + +.theme-key-light .ui-sortable-handle { + border-radius: 6px; + color: color-mix(in srgb, var(--admin-muted) 82%, var(--admin-text)); +} + +.theme-key-light .ui-sortable-handle:hover, +.theme-key-light .ui-sortable-handle:focus-visible { + background: rgba(15, 23, 42, 0.055); + color: color-mix(in srgb, var(--accent) 58%, #0f172a); +} + +.theme-key-light .ui-sortable-item.is-drop-target { + outline-color: color-mix(in srgb, var(--accent) 48%, #0f172a); + background: color-mix(in srgb, var(--accent) 8%, #ffffff); +} + /* Telegram notifications banner: keep the warm warning tint but swap the * dark-theme inset bevel for the soft drop shadow the other light cards use. */ .theme-key-light .telegram-notifications-card { diff --git a/backend/bot/app/web/themes/light/theme.json b/backend/bot/app/web/themes/light/theme.json index 1a75f47..a501c88 100644 --- a/backend/bot/app/web/themes/light/theme.json +++ b/backend/bot/app/web/themes/light/theme.json @@ -9,7 +9,7 @@ "use_primary_accent": true, "use_in_admin": true, "css_file": "style.css", - "assets_version": 4, + "assets_version": 5, "tokens": { "color_scheme": "light" } diff --git a/backend/bot/app/web/themes/windows95/style.css b/backend/bot/app/web/themes/windows95/style.css index cd896f9..3e106d9 100644 --- a/backend/bot/app/web/themes/windows95/style.css +++ b/backend/bot/app/web/themes/windows95/style.css @@ -127,6 +127,7 @@ .theme-key-windows95 svg.lucide-file-text, .theme-key-windows95 svg.lucide-gift, .theme-key-windows95 svg.lucide-globe-2, +.theme-key-windows95 svg.lucide-grip-vertical, .theme-key-windows95 svg.lucide-home, .theme-key-windows95 svg.lucide-house, .theme-key-windows95 svg.lucide-info, @@ -232,6 +233,10 @@ --win95-button-icon: var(--win95-icon-globe); } +.theme-key-windows95 svg.lucide-grip-vertical { + --win95-button-icon: var(--win95-icon-sliders); +} + .theme-key-windows95 svg.lucide-file-text { --win95-button-icon: var(--win95-icon-file-text); } @@ -375,6 +380,7 @@ svg.lucide-file-text, svg.lucide-gift, svg.lucide-globe-2, + svg.lucide-grip-vertical, svg.lucide-home, svg.lucide-house, svg.lucide-info, @@ -1171,6 +1177,86 @@ body:has(.theme-key-windows95) .install-platform-item[data-selected] { opacity: 0.52; } +/* Admin controls: range sliders and sortable rows */ + +.theme-key-windows95 .ui-range-input { + height: 22px; +} + +.theme-key-windows95 .ui-range-input::before { + height: 8px; + border: 2px solid; + border-color: #404040 #ffffff #ffffff #404040; + border-radius: 0 !important; + background: #ffffff; + box-shadow: + inset 1px 1px 0 #808080, + inset -1px -1px 0 #dfdfdf; +} + +.theme-key-windows95 .ui-range-input__range { + height: 8px; + border-radius: 0 !important; + background: var(--accent); +} + +.theme-key-windows95 .ui-range-input__thumb { + width: 14px; + height: 20px; + border: 2px solid; + border-color: #ffffff #404040 #404040 #ffffff; + border-radius: 0 !important; + background: #c0c0c0; + box-shadow: + inset 1px 1px 0 #dfdfdf, + inset -1px -1px 0 #808080; + transition: none; +} + +.theme-key-windows95 .ui-range-input__thumb:hover, +.theme-key-windows95 .ui-range-input__thumb:focus-visible { + background: #dfdfdf; +} + +.theme-key-windows95 .ui-range-input__thumb[data-active] { + border-color: #404040 #ffffff #ffffff #404040; + box-shadow: + inset 1px 1px 0 #808080, + inset -1px -1px 0 #dfdfdf; +} + +.theme-key-windows95 .ui-sortable-item.is-drop-target { + outline: 1px dotted #000000; + outline-offset: 3px; + background: color-mix(in srgb, var(--accent) 12%, var(--admin-surface)); +} + +.theme-key-windows95 .ui-sortable-handle { + align-self: center; + width: 24px; + height: 28px; + border: 2px solid; + border-color: #ffffff #404040 #404040 #ffffff; + background: #c0c0c0; + color: #000000; + box-shadow: + inset 1px 1px 0 #dfdfdf, + inset -1px -1px 0 #808080; + transition: none; +} + +.theme-key-windows95 .ui-sortable-handle:hover, +.theme-key-windows95 .ui-sortable-handle:focus-visible { + background: #dfdfdf; +} + +.theme-key-windows95 .ui-sortable-handle:active { + border-color: #404040 #ffffff #ffffff #404040; + box-shadow: + inset 1px 1px 0 #808080, + inset -1px -1px 0 #dfdfdf; +} + .theme-key-windows95 input::placeholder, .theme-key-windows95 textarea::placeholder, .theme-key-windows95 .input::placeholder, diff --git a/backend/bot/app/web/themes/windows95/theme.json b/backend/bot/app/web/themes/windows95/theme.json index 2eb8936..aea131f 100644 --- a/backend/bot/app/web/themes/windows95/theme.json +++ b/backend/bot/app/web/themes/windows95/theme.json @@ -9,7 +9,7 @@ "use_primary_accent": false, "use_in_admin": true, "css_file": "style.css", - "assets_version": 12, + "assets_version": 13, "tokens": { "color_scheme": "light", "style_preset": "win95" diff --git a/backend/config/webapp_themes_config.py b/backend/config/webapp_themes_config.py index d3e527c..bae3ab6 100644 --- a/backend/config/webapp_themes_config.py +++ b/backend/config/webapp_themes_config.py @@ -371,6 +371,7 @@ def _builtin_theme_assets_need_refresh(key: str, target_dir: Path) -> bool: "--success-text" not in style or ".theme-key-light.app-shell" not in style or "Install guide theme surfaces" not in style + or "Admin controls: range sliders and sortable rows" not in style ) if key == "ascii": return ( @@ -381,6 +382,7 @@ def _builtin_theme_assets_need_refresh(key: str, target_dir: Path) -> bool: or "Console-style tables" not in style or "New webapp surfaces: support, purchase info, password login" not in style or "Install guide theme surfaces" not in style + or "Admin controls: range sliders and sortable rows" not in style ) if key != "windows95": return False @@ -404,6 +406,7 @@ def _builtin_theme_assets_need_refresh(key: str, target_dir: Path) -> bool: or "lucide-qr-code" not in style or "New webapp surfaces: support, purchase info, password login" not in style or "Install guide theme surfaces" not in style + or "Admin controls: range sliders and sortable rows" not in style or any(not (target_dir / "icons" / icon).exists() for icon in required_icons) ) diff --git a/tests/test_webapp_assets.py b/tests/test_webapp_assets.py index 50b09d9..c7b7c24 100644 --- a/tests/test_webapp_assets.py +++ b/tests/test_webapp_assets.py @@ -568,6 +568,24 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase): self.assertIn("--home-logo-scale-mobile", base_css) self.assertIn("--home-logo-scale-desktop", base_css) + def test_builtin_css_themes_cover_admin_range_and_sortable_controls(self): + theme_root = Path("backend/bot/app/web/themes") + required_selectors = ( + "ui-range-input", + "ui-range-input__thumb", + "ui-sortable-item", + "ui-sortable-handle", + "is-drop-target", + ) + + for key in ("light", "ascii", "windows95"): + css = (theme_root / key / "style.css").read_text(encoding="utf-8") + for selector in required_selectors: + self.assertIn(selector, css, f"{key} theme must style {selector}") + + windows95_css = (theme_root / "windows95" / "style.css").read_text(encoding="utf-8") + self.assertIn("lucide-grip-vertical", windows95_css) + def test_prune_unused_appearance_assets_keeps_only_referenced_logo_and_favicons(self): with tempfile.TemporaryDirectory() as tmpdir: root = Path(tmpdir) diff --git a/tests/test_webapp_themes_config.py b/tests/test_webapp_themes_config.py index 2fb943d..d697947 100644 --- a/tests/test_webapp_themes_config.py +++ b/tests/test_webapp_themes_config.py @@ -33,14 +33,14 @@ class WebappThemesConfigTests(unittest.TestCase): self.assertEqual(win95.tokens.style_preset, "win95") self.assertFalse(win95.use_primary_accent) self.assertTrue(win95.use_in_admin) - self.assertEqual(win95.assets_version, 12) - self.assertEqual(cfg.theme_by_key("light").assets_version, 4) + self.assertEqual(win95.assets_version, 13) + self.assertEqual(cfg.theme_by_key("light").assets_version, 5) ascii_theme = cfg.theme_by_key("ascii") self.assertIsNotNone(ascii_theme) self.assertEqual(ascii_theme.css_file, "style.css") self.assertFalse(ascii_theme.use_primary_accent) self.assertTrue(ascii_theme.use_in_admin) - self.assertEqual(ascii_theme.assets_version, 5) + self.assertEqual(ascii_theme.assets_version, 6) def test_env_override_default_theme(self): cfg = builtin_webapp_themes_config("#00fe7a") @@ -391,7 +391,7 @@ class WebappThemesConfigTests(unittest.TestCase): descriptor["assets_version"], cfg.theme_by_key("windows95").assets_version, ) - self.assertEqual(descriptor["assets_version"], 12) + self.assertEqual(descriptor["assets_version"], 13) self.assertIn("lucide-house", css) self.assertIn("lucide-earth", css) self.assertIn("lucide-circle-check", css) @@ -404,6 +404,7 @@ class WebappThemesConfigTests(unittest.TestCase): self.assertIn("lucide-qr-code", css) self.assertIn("New webapp surfaces: support, purchase info, password login", css) self.assertIn("Install guide theme surfaces", css) + self.assertIn("Admin controls: range sliders and sortable rows", css) self.assertIn( ( ".theme-key-windows95 .support-list-card {\n" @@ -445,6 +446,7 @@ class WebappThemesConfigTests(unittest.TestCase): css = (stale_theme_dir / "style.css").read_text(encoding="utf-8") self.assertIn(".theme-key-light.app-shell", css) self.assertIn("Install guide theme surfaces", css) + self.assertIn("Admin controls: range sliders and sortable rows", css) def test_resolved_refreshes_stale_builtin_ascii_assets(self): with tempfile.TemporaryDirectory() as tmp: @@ -477,6 +479,7 @@ class WebappThemesConfigTests(unittest.TestCase): descriptor = json.loads((stale_theme_dir / "theme.json").read_text(encoding="utf-8")) css = (stale_theme_dir / "style.css").read_text(encoding="utf-8") self.assertEqual(descriptor["assets_version"], cfg.theme_by_key("ascii").assets_version) - self.assertEqual(descriptor["assets_version"], 5) + self.assertEqual(descriptor["assets_version"], 6) self.assertIn("Console-style tables", css) self.assertIn("Install guide theme surfaces", css) + self.assertIn("Admin controls: range sliders and sortable rows", css)