diff --git a/3DTank/docs/SettingsStyleTable.md b/3DTank/docs/SettingsStyleTable.md
index f4a387b..241d89c 100644
--- a/3DTank/docs/SettingsStyleTable.md
+++ b/3DTank/docs/SettingsStyleTable.md
@@ -91,4 +91,38 @@
| 滑杆进度渐变 | `hUi.tap` | `module_82347.js:200507` |
| `SettingsComponentStyle-slider` | `kNi.z1jp_1` | `module_82347.js:89620` |
-`ksc-130` 是 `SettingsComponentStyle-slider` 的 Emotion 生成类名;当前实现用 `data-ui="SettingsComponentStyle-slider"` 标记,等价于给滑杆挂上该样式。
+`ksc-130` 是 `SettingsComponentStyle-slider` 的 Emotion 生成类名;当前实现直接使用 `InputRangeComponentStyle-*` / `SettingsComponentStyle-slider` 语义类名,并用 `ksc-56` / `ksc-57` 标记标签与滑杆。
+
+## 7. 真实 DOM 结构
+
+滑杆:
+
+```html
+
+```
+
+开关:
+
+```html
+
+
+
+
+
+
+
+
+
+
+```
+
+当前实现已按这个结构输出;带描述的开关行增加 `has-description` 修饰类,让描述与标签对齐。
diff --git a/TankiOffline/app/src/jsMain/kotlin/App/Browser/Ui/LobbyScreens.kt b/TankiOffline/app/src/jsMain/kotlin/App/Browser/Ui/LobbyScreens.kt
index 7597f27..4b342e5 100644
--- a/TankiOffline/app/src/jsMain/kotlin/App/Browser/Ui/LobbyScreens.kt
+++ b/TankiOffline/app/src/jsMain/kotlin/App/Browser/Ui/LobbyScreens.kt
@@ -24,6 +24,7 @@ import react.dom.html.ReactHTML.h3
import react.dom.html.ReactHTML.i
import react.dom.html.ReactHTML.img
import react.dom.html.ReactHTML.input
+import react.dom.html.ReactHTML.label
import react.dom.html.ReactHTML.option
import react.dom.html.ReactHTML.p
import react.dom.html.ReactHTML.select
@@ -2356,37 +2357,6 @@ private val SettingsChevron: csstype.PropertiesBuilder.() -> Unit = {
Style("flex", "0 0 0.813em")
}
-private val SettingsSliderWrap: csstype.PropertiesBuilder.() -> Unit = {
- Style("display", "flex")
- Style("flex-direction", "column")
- Style("justify-content", "space-between")
- Style("align-items", "flex-start")
- Style("width", "31.25em")
- Style("height", "3.188em")
- Style("min-height", "3.188em")
- Style("margin-bottom", "1.75em")
-}
-
-private val SettingsSliderLabel: csstype.PropertiesBuilder.() -> Unit = {
- Style("display", "flex")
- Style("justify-content", "space-between")
- Style("align-items", "center")
- Style("width", "100%")
- Style("color", "white")
- Style("font-size", "0.9375em")
- Style("margin-top", "0")
-}
-
-private val SettingsRange: csstype.PropertiesBuilder.() -> Unit = {
- Style("cursor", "pointer")
- Style("-webkit-appearance", "none")
- Style("appearance", "none")
- Style("width", "100%")
- Style("height", "0.563em")
- Style("outline", "none")
- Style("border-radius", "0")
-}
-
private fun SettingsSliderGradient(Value: Int): String {
val Percent = Value.coerceIn(0, 100)
return "linear-gradient(to right, #ffbc09 0%, #ffbc09 $Percent%, rgba(255, 255, 255, 0.1) $Percent%, rgba(255, 255, 255, 0.1) 100%)"
@@ -2430,46 +2400,6 @@ private val SettingsActionButton: csstype.PropertiesBuilder.() -> Unit = {
Style("padding", "0 1em")
}
-private val SettingsCheckboxRow: csstype.PropertiesBuilder.() -> Unit = {
- Style("display", "flex")
- Style("align-items", "center")
- Style("justify-content", "flex-start")
- Style("width", "100%")
- Style("height", "2.25em")
- Style("margin-bottom", "1.25em")
- Style("gap", "0.75em")
-}
-
-private fun SettingsCheckbox(On: Boolean): csstype.PropertiesBuilder.() -> Unit = {
- Style("position", "relative")
- Style("display", "flex")
- Style("align-items", "center")
- Style("justify-content", "center")
- Style("width", "2.75em")
- Style("height", "1.5em")
- Style("flex", "0 0 2.75em")
- Style("box-sizing", "border-box")
- Style("background-color", if (On) "rgba(118, 255, 51, 0.25)" else "rgba(191, 213, 255, 0.25)")
- Style("border", "0.063em solid rgba(255, 255, 255, 0.2)")
- Style("border-radius", "6.25rem")
- Style("cursor", "pointer")
- Style("padding", "0")
- Style("transition", "background-color 0.2s")
-}
-
-private fun SettingsToggleKnob(On: Boolean): csstype.PropertiesBuilder.() -> Unit = {
- Style("position", "absolute")
- Style("top", "50%")
- Style("left", if (On) "calc(100% - 1.3em)" else "0.2em")
- Style("width", "1.1em")
- Style("height", "1.1em")
- Style("border-radius", "50%")
- Style("background-color", "white")
- Style("transform", "translateY(-50%)")
- Style("transition", "left 0.2s")
- Style("pointer-events", "none")
-}
-
private val SettingsKeysGrid: csstype.PropertiesBuilder.() -> Unit = {
Style("display", "grid")
Style("grid-template-columns", "1fr 1fr")
@@ -2570,41 +2500,57 @@ private val SettingsScreen = FC {
is SettingEntry.Toggle -> {
val On = Toggles[Entry.Id] ?: Entry.Default
div {
- css(SettingsCheckboxRow)
- Ui("CheckBoxComponentStyle-oneSetting")
- button {
- css(SettingsCheckbox(On))
- onClick = { Toggles = Toggles + (Entry.Id to !On) }
- div { css(SettingsToggleKnob(On)) }
- }
+ asDynamic()["className"] = "ksc-46"
div {
- css { Style("flex", "1"); Style("min-width", "0") }
- div { css(SettingsLabel); +Entry.Label }
- if (Entry.Desc.isNotEmpty()) div { css(SettingsDesc); +Entry.Desc }
+ asDynamic()["className"] = "CheckBoxComponentStyle-oneSetting" + if (Entry.Desc.isNotEmpty()) " has-description" else ""
+ div {
+ asDynamic()["className"] = "CheckBoxStyle-checkbox"
+ label {
+ input {
+ asDynamic()["className"] = "ksc-47"
+ asDynamic()["type"] = "checkbox"
+ asDynamic()["checked"] = On
+ onChange = { Toggles = Toggles + (Entry.Id to !On) }
+ }
+ span {
+ asDynamic()["className"] = "ksc-62 -displayFlex"
+ asDynamic()["data-on"] = On.toString()
+ }
+ }
+ }
+ div {
+ asDynamic()["className"] = "ksc-63 -flexStartAlignCenter" + if (Entry.Desc.isNotEmpty()) " has-description" else ""
+ label { +Entry.Label }
+ if (Entry.Desc.isNotEmpty()) div { css(SettingsDesc); +Entry.Desc }
+ }
}
}
}
is SettingEntry.Slider -> {
val Value = Sliders[Entry.Id] ?: Entry.Default
div {
- css(SettingsSliderWrap)
- Ui("InputRangeComponentStyle-blockSoundVolume")
+ asDynamic()["className"] = "InputRangeComponentStyle-blockSoundVolume SettingsComponentStyle-slider"
+ p {
+ +Entry.Label
+ span {
+ asDynamic()["className"] = "ksc-56"
+ +"($Value%)"
+ }
+ }
div {
- css(SettingsSliderLabel)
- span { +Entry.Label }
- span { +"$Value%" }
- }
- input {
- css {
- Apply(SettingsRange)
- Style("background", SettingsSliderGradient(Value))
+ asDynamic()["className"] = "InputRangeComponentStyle-soundVolume"
+ input {
+ asDynamic()["className"] = "ksc-57 InputRangeComponentStyle-range"
+ asDynamic()["type"] = "range"
+ asDynamic()["min"] = "0"
+ asDynamic()["max"] = "100"
+ asDynamic()["step"] = "1"
+ asDynamic()["value"] = "$Value"
+ val StyleObj = js("({})")
+ StyleObj["background"] = SettingsSliderGradient(Value)
+ asDynamic()["style"] = StyleObj
+ onChange = { E -> Sliders = Sliders + (Entry.Id to (E.target.value.toString().toIntOrNull() ?: Value)) }
}
- Ui("SettingsComponentStyle-slider")
- asDynamic()["type"] = "range"
- asDynamic()["min"] = "0"
- asDynamic()["max"] = "100"
- asDynamic()["value"] = "$Value"
- onChange = { E -> Sliders = Sliders + (Entry.Id to (E.target.value.toString().toIntOrNull() ?: Value)) }
}
}
}
diff --git a/TankiOffline/app/src/jsMain/resources/lobby3.css b/TankiOffline/app/src/jsMain/resources/lobby3.css
index a54d7bd..4963084 100644
--- a/TankiOffline/app/src/jsMain/resources/lobby3.css
+++ b/TankiOffline/app/src/jsMain/resources/lobby3.css
@@ -984,4 +984,132 @@ body { margin: 0; overflow: hidden; background: #001926; color: #fff; }
border: 0;
border-radius: 0.25em;
background: #ffbc09;
+}
+
+/* 原版设置页语义类 */
+.-displayFlex { display: flex; }
+.-flexStartAlignCenter { display: flex; align-items: center; justify-content: flex-start; }
+
+.InputRangeComponentStyle-blockSoundVolume {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: flex-start;
+ width: 31.25em;
+ height: 3.188em;
+ min-height: 3.188em;
+ margin-bottom: 1.75em;
+}
+.SettingsComponentStyle-slider { width: 31.25em; }
+.InputRangeComponentStyle-blockSoundVolume p {
+ margin: 0;
+ color: #fff;
+ font-family: BaseFontRegular, "Segoe UI", sans-serif;
+ font-size: 0.9375em;
+ line-height: normal;
+}
+.InputRangeComponentStyle-blockSoundVolume p span { margin-left: 0.2em; }
+.InputRangeComponentStyle-soundVolume {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ height: 1.25em;
+}
+.InputRangeComponentStyle-range {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 100%;
+ height: 0.563em;
+ outline: none;
+ cursor: pointer;
+ border-radius: 0;
+}
+.InputRangeComponentStyle-range::-webkit-slider-runnable-track {
+ width: 100%;
+ height: 0.375em;
+ background: transparent;
+}
+.InputRangeComponentStyle-range::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 1.875em;
+ height: 1em;
+ border-radius: 0.25em;
+ background: #ffbc09;
+ margin-top: -0.3em;
+}
+.InputRangeComponentStyle-range::-moz-range-track { background: transparent; }
+.InputRangeComponentStyle-range::-moz-range-progress { background: #ffbc09; }
+.InputRangeComponentStyle-range::-moz-range-thumb {
+ width: 1.875em;
+ height: 1em;
+ border: 0;
+ border-radius: 0.25em;
+ background: #ffbc09;
+}
+
+.CheckBoxComponentStyle-oneSetting {
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ width: 100%;
+ height: 2.25em;
+ margin-bottom: 1.25em;
+}
+.CheckBoxComponentStyle-oneSetting.has-description {
+ height: auto;
+ min-height: 2.25em;
+ align-items: flex-start;
+}
+.CheckBoxStyle-checkbox { display: flex; z-index: 1; }
+.CheckBoxStyle-checkbox label {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+}
+.CheckBoxStyle-checkbox input {
+ position: absolute;
+ width: 0;
+ height: 0;
+ opacity: 0;
+ pointer-events: none;
+}
+.ksc-62 {
+ position: relative;
+ display: flex;
+ width: 2.75em;
+ height: 1.5em;
+ border: 0.063em solid rgba(255, 255, 255, 0.2);
+ border-radius: 6.25rem;
+ background: rgba(191, 213, 255, 0.25);
+ transition: background-color 0.2s;
+}
+.ksc-62[data-on="true"] { background: rgba(118, 255, 51, 0.25); }
+.ksc-62::after {
+ content: "";
+ position: absolute;
+ top: 50%;
+ left: 0.2em;
+ width: 1.1em;
+ height: 1.1em;
+ border-radius: 50%;
+ background: #fff;
+ transform: translateY(-50%);
+ transition: left 0.2s;
+}
+.ksc-62[data-on="true"]::after { left: calc(100% - 1.3em); }
+.ksc-63 { display: flex; align-items: center; justify-content: flex-start; }
+.ksc-63.has-description {
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: flex-start;
+ gap: 0.15em;
+ padding-top: 0.2em;
+}
+.ksc-63 label {
+ color: rgba(255, 255, 255, 0.9);
+ font-family: BaseFontRegular, "Segoe UI", sans-serif;
+ font-size: 1em;
+ line-height: normal;
}
\ No newline at end of file