From 8d881df3cc9be5f13fe714c4ba069d4d62f564bf Mon Sep 17 00:00:00 2001 From: Sin-Kang Date: Fri, 13 Nov 2026 11:52:20 +0900 Subject: [PATCH] =?UTF-8?q?fix(segmented):=20the=20chosen=20face=20is=20bg?= =?UTF-8?q?.raised=20=E2=80=94=20lighter=20than=20the=20track=20in=20dark?= =?UTF-8?q?=20(D-054,=20Android=200.8.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the dark theme the chosen segment was bg.default (#09090B), darker than its bg.subtle track (#18181B), and the level-1 shadow does not show in dark, so it read as a hole (found in Qooroo's full-screen review). Web and Android alike. New semantic token color.bg.raised: white in light (unchanged look), zinc.800 (#27272A) in dark, one step lighter than bg.subtle. Text on it: 14.27:1 primary, 5.81:1 secondary. The chosen segment uses it in dds-css (so react, solid and vue) and in DdsSegmentedControl (DdsColors.bgRaised). The shadow stays at level 1, as the owner decided; Android guide §10 no. 20 is settled. Tests pin it: tokens (raised lighter than subtle, text >= 4.5:1, the CSS uses it), ColorSchemeTest, and a device test that the dark face is lighter than the track. Android 0.8.1; a minor changeset for dds-tokens and dds-css. --- .changeset/segmented-raised-face.md | 6 +++ README.ko.md | 2 +- README.md | 2 +- android/CHANGELOG.ko.md | 14 +++++++ android/CHANGELOG.md | 16 ++++++++ android/README.ko.md | 4 +- android/README.md | 4 +- .../dds/compose/SegmentedOnDeviceTest.kt | 15 ++++++- .../dds/compose/component/Segmented.kt | 4 +- .../kr/devslab/dds/compose/theme/DdsColors.kt | 8 ++++ .../dds/compose/theme/ColorSchemeTest.kt | 18 ++++++++ .../kr/devslab/dds/tokens/DdsColorTokens.kt | 2 + .../kr/devslab/dds/tokens/DdsTokensInfo.kt | 2 +- .../src/main/res/values-night/dds_tokens.xml | 1 + .../src/main/res/values/dds_tokens.xml | 1 + android/gradle.properties | 2 +- docs/backlog.md | 2 +- docs/decisions.md | 41 +++++++++++++++++++ docs/design-system.ko.md | 1 + docs/design-system.md | 1 + docs/mobile-native.ko.md | 13 +++--- docs/mobile-native.md | 14 ++++--- packages/dds-css/src/segmented-control.css | 4 +- preview/index.html | 1 + tests/foundation-contracts.test.mjs | 30 ++++++++++++++ tokens/semantic.dark.json | 1 + tokens/semantic.light.json | 1 + 27 files changed, 185 insertions(+), 25 deletions(-) create mode 100644 .changeset/segmented-raised-face.md diff --git a/.changeset/segmented-raised-face.md b/.changeset/segmented-raised-face.md new file mode 100644 index 00000000..2307a98b --- /dev/null +++ b/.changeset/segmented-raised-face.md @@ -0,0 +1,6 @@ +--- +"@devslab/dds-tokens": minor +"@devslab/dds-css": minor +--- + +New semantic token `color.bg.raised` (`--dds-color-bg-raised`): white in light, `zinc.800` in dark — a face raised above `bg.subtle`. The chosen SegmentedControl segment uses it, so in the dark theme it is one step lighter than its track instead of darker (it read as a hole). The light theme looks the same; the level-1 shadow stays (D-054). diff --git a/README.ko.md b/README.ko.md index 4791d466..3ca80355 100644 --- a/README.ko.md +++ b/README.ko.md @@ -120,7 +120,7 @@ Source-Available License 1.0을 따른다. compatibility canary는 비공개· 때만 만든다 (명세 §8): `@devslab/dds-tokens` (✅ 2026-08-13) → `dds-css` v1 인벤토리 (Core 6종 ✅ 2026-08-13, +13종 ✅ 2026-08-15) → AskLinq 적용 → `dds-icons` (✅ 2026-08-15) → `dds-solid` + `site-kit` → 안드로이드 네이티브 값·테마·모션 -(0.6.0, 2026-11-12, Maven Central — 테마·모션·부품·아이콘). 0.7.0(2026-11-13, Maven Central)에서 화면 틀과 `dds-compose-testing` 꾸러미가 생겼다(D-052). 0.8.0(2026-11-13)에서 `DdsSegmentedControl`이 생겼다. RN·Ionic 패키지는 그 제품이 생길 때. +(0.6.0, 2026-11-12, Maven Central — 테마·모션·부품·아이콘). 0.7.0(2026-11-13, Maven Central)에서 화면 틀과 `dds-compose-testing` 꾸러미가 생겼다(D-052). 0.8.0(2026-11-13)에서 `DdsSegmentedControl`이 생겼고, 0.8.1에서 어두운 화면의 고른 칸이 밝아졌다(`bg.raised`, D-054). RN·Ionic 패키지는 그 제품이 생길 때. ## 입력·선택·공유 달력 확장 diff --git a/README.md b/README.md index 2649ba59..b43be1ce 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Priorities live in [`docs/backlog.md`](docs/backlog.md). Packages are created only when a consumer exists (spec §8): `@devslab/dds-tokens` (✅ 2026-08-13) → `dds-css` v1 inventory (Core 6 ✅ 2026-08-13, +13 ✅ 2026-08-15) → AskLinq adoption → `dds-icons` (✅ 2026-08-15) → `dds-solid` + `site-kit` → native Android values, theme and motion -(0.6.0, 2026-11-12, on Maven Central — theme, motion, components, icons). 0.7.0 (2026-11-13, on Maven Central) adds the screen frame and the `dds-compose-testing` artifact (D-052). 0.8.0 (2026-11-13) adds `DdsSegmentedControl`. RN and Ionic packages only when such a product exists. +(0.6.0, 2026-11-12, on Maven Central — theme, motion, components, icons). 0.7.0 (2026-11-13, on Maven Central) adds the screen frame and the `dds-compose-testing` artifact (D-052). 0.8.0 (2026-11-13) adds `DdsSegmentedControl`, and 0.8.1 its lighter chosen face in dark (`bg.raised`, D-054). RN and Ionic packages only when such a product exists. ## Input, choice and shared calendar additions diff --git a/android/CHANGELOG.ko.md b/android/CHANGELOG.ko.md index 71d9d782..c9340302 100644 --- a/android/CHANGELOG.ko.md +++ b/android/CHANGELOG.ko.md @@ -5,6 +5,20 @@ `kr.devslab:dds-android-tokens`, `kr.devslab:dds-compose`, 그리고 0.7.0부터 `kr.devslab:dds-compose-testing`은 한 버전을 쓰고, 웹 패키지와는 버전이 따로다(D-048). 릴리스는 `android-v<버전>` 태그로 낸다. +## 0.8.1 — 2026-11-13 + +**바뀐 것** + +- **어두운 화면에서 고른 세그먼트가 트랙보다 밝다(D-054).** 0.8.0의 고른 칸은 `bg.default`(`#09090B`)라 트랙 + `bg.subtle`(`#18181B`)보다 어두웠고, 그림자는 어두운 화면에서 보이지 않아 구멍처럼 보였다(Qooroo 전체 화면 점검). + 이제 새 토큰 `bg.raised`(`#27272A`, `zinc.800`)로 칠한다. 밝은 화면은 그대로 흰 면이다. 그림자는 1단계 그대로(소유자 + 결정, 가이드 §10의 20번 확정). 웹 SegmentedControl도 같이 바뀌었다. + +**새로 생긴 것** + +- **`DdsColors.bgRaised`, 토큰 `DdsLightColors.bgRaised` / `DdsDarkColors.bgRaised`, 리소스 `dds_color_bg_raised`** — + 옅은 바탕 위로 올라온 면. 밝은 `#FFFFFF`, 어두운 `#27272A`. 제품이 정하는 바탕(D-053)이 아니라 시스템 값이다. + ## 0.8.0 — 2026-11-13 **새로 생긴 것** diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 9ed55b4d..0e94a153 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -5,6 +5,22 @@ `kr.devslab:dds-android-tokens`, `kr.devslab:dds-compose` and (from 0.7.0) `kr.devslab:dds-compose-testing` share one version, separate from the web packages (D-048). Releases are cut by an `android-v` tag. +## 0.8.1 — 2026-11-13 + +**Changed** + +- **In the dark theme the chosen segment is lighter than the track (D-054).** 0.8.0 painted it `bg.default` (`#09090B`), + darker than the `bg.subtle` track (`#18181B`), and the shadow does not show in dark, so it read as a hole (found in + Qooroo's full-screen review). It is now the new `bg.raised` token (`#27272A`, `zinc.800`). The light theme is unchanged, + a white face. The shadow stays at level 1 (the owner's decision; guide §10 no. 20 is settled). The web + SegmentedControl changed with it. + +**New** + +- **`DdsColors.bgRaised`, tokens `DdsLightColors.bgRaised` / `DdsDarkColors.bgRaised`, resource `dds_color_bg_raised`** — + a face raised above the quiet background. `#FFFFFF` in light, `#27272A` in dark. A system value, not one of the + backgrounds a product sets (D-053). + ## 0.8.0 — 2026-11-13 **New** diff --git a/android/README.ko.md b/android/README.ko.md index 5e37fafa..57d88fda 100644 --- a/android/README.ko.md +++ b/android/README.ko.md @@ -26,8 +26,8 @@ dependencies { ```kotlin dependencies { - implementation("kr.devslab:dds-compose:0.8.0") - androidTestImplementation("kr.devslab:dds-compose-testing:0.8.0") + implementation("kr.devslab:dds-compose:0.8.1") + androidTestImplementation("kr.devslab:dds-compose-testing:0.8.1") } ``` diff --git a/android/README.md b/android/README.md index 5f7bfb46..647623df 100644 --- a/android/README.md +++ b/android/README.md @@ -26,8 +26,8 @@ dependencies { ```kotlin dependencies { - implementation("kr.devslab:dds-compose:0.8.0") - androidTestImplementation("kr.devslab:dds-compose-testing:0.8.0") + implementation("kr.devslab:dds-compose:0.8.1") + androidTestImplementation("kr.devslab:dds-compose-testing:0.8.1") } ``` diff --git a/android/dds-compose/src/androidTest/kotlin/kr/devslab/dds/compose/SegmentedOnDeviceTest.kt b/android/dds-compose/src/androidTest/kotlin/kr/devslab/dds/compose/SegmentedOnDeviceTest.kt index f57768b8..7d12f7b4 100644 --- a/android/dds-compose/src/androidTest/kotlin/kr/devslab/dds/compose/SegmentedOnDeviceTest.kt +++ b/android/dds-compose/src/androidTest/kotlin/kr/devslab/dds/compose/SegmentedOnDeviceTest.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PixelMap +import androidx.compose.ui.graphics.luminance import androidx.compose.ui.graphics.toPixelMap import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.Role @@ -223,7 +224,7 @@ class SegmentedOnDeviceTest { // Read over the unchosen segment, clear of the chosen face's shadow. assertColor("the track between its line and the faces", colors.bgSubtle, pixels[other.center.x.toInt(), (2 * oneDp).toInt()]) assertColor("an unchosen segment shows the track", colors.bgSubtle, pixels[(other.right - 6 * oneDp).toInt(), midY]) - assertColor("the chosen face", colors.bgDefault, pixels[(chosen.left + 6 * oneDp).toInt(), midY]) + assertColor("the chosen face", colors.bgRaised, pixels[(chosen.left + 6 * oneDp).toInt(), midY]) assertColor("the chosen face's line", colors.borderStrong, pixels[(chosen.left + half).toInt(), midY]) } @@ -239,6 +240,18 @@ class SegmentedOnDeviceTest { assertDrawnWith(line = colors.borderStrong) } + @Test + fun inTheDarkThemeTheChosenFaceIsLighterThanTheTrackNotAHole() { + show(dark = true) { DdsSegmentedControl(kinds, "person", {}, Modifier.testTag("control"), contentDescription = "종류", fullWidth = true) } + val pixels = control() + val chosen = inControl("개인") + val other = inControl("비즈니스") + val face = pixels[(chosen.left + 6 * oneDp).toInt(), chosen.center.y.toInt()] + val track = pixels[(other.right - 6 * oneDp).toInt(), other.center.y.toInt()] + assertTrue("face $face is lighter than track $track", face.luminance() > track.luminance()) + assertTrue("and not the page's darker bgDefault", !close(face, colors.bgDefault)) + } + @Test fun anErrorTurnsTheTrackLineTheDangerColorAndSaysWhy() { show { diff --git a/android/dds-compose/src/main/kotlin/kr/devslab/dds/compose/component/Segmented.kt b/android/dds-compose/src/main/kotlin/kr/devslab/dds/compose/component/Segmented.kt index 7b5a508a..b355b200 100644 --- a/android/dds-compose/src/main/kotlin/kr/devslab/dds/compose/component/Segmented.kt +++ b/android/dds-compose/src/main/kotlin/kr/devslab/dds/compose/component/Segmented.kt @@ -70,7 +70,7 @@ public class DdsSegment( * - **Track:** `bg.subtle`, a 1dp `border.strong` line, `radius.md`, 2dp inside * it and 2dp between segments. * - **Segment:** `radius.sm`, the `label` type, `text.secondary`. The chosen - * one is a `bg.default` face with a 1dp `border.strong` line, a level-1 + * one is a `bg.raised` face with a 1dp `border.strong` line, a level-1 * shadow, `text.primary` and weight 600. * - **Height:** one size, the web's touch height. The track is 50dp (48dp * inside its line); each segment is 48dp of touch with a 44dp face and is @@ -231,7 +231,7 @@ private fun Segment( val active = enabled && !readOnly && !selected val face by colorChange( when { - selected -> colors.bgDefault + selected -> colors.bgRaised active && pressed -> colors.overlayPressedTint active && hovered -> colors.overlayHoverTint else -> colors.overlayPressedTint.clear() diff --git a/android/dds-compose/src/main/kotlin/kr/devslab/dds/compose/theme/DdsColors.kt b/android/dds-compose/src/main/kotlin/kr/devslab/dds/compose/theme/DdsColors.kt index f19d1f5d..d49d3c51 100644 --- a/android/dds-compose/src/main/kotlin/kr/devslab/dds/compose/theme/DdsColors.kt +++ b/android/dds-compose/src/main/kotlin/kr/devslab/dds/compose/theme/DdsColors.kt @@ -27,6 +27,12 @@ public class DdsColors internal constructor( public val bgSubtle: Color, /** Cards, sheets, dialogs, menus. The product's, if its brand names one. */ public val bgElevated: Color, + /** + * A face raised above [bgSubtle]: the chosen segment of a segmented control. + * White in light; in dark a step lighter than [bgSubtle] (where [bgDefault] + * would read as a hole). Always the system's: a brand does not set it. + */ + public val bgRaised: Color, /** Filled brand face — the primary button. The same in light and dark. */ public val bgBrand: Color, /** [bgBrand] while pressed. (The web calls it hover; touch has no hover.) */ @@ -80,6 +86,7 @@ internal fun ddsColors(dark: Boolean, brand: DdsBrand): DdsColors = bgDefault = pageBackground(brand.dark, night = true), bgSubtle = subtleBackground(brand.dark, night = true), bgElevated = cardBackground(brand.dark, night = true), + bgRaised = Color(DdsDarkColors.bgRaised), bgBrand = brand.dark.bgBrand, bgBrandHover = brand.dark.bgBrandHover, bgBrandSubtle = brand.dark.bgBrandSubtle, @@ -112,6 +119,7 @@ internal fun ddsColors(dark: Boolean, brand: DdsBrand): DdsColors = bgDefault = pageBackground(brand.light, night = false), bgSubtle = subtleBackground(brand.light, night = false), bgElevated = cardBackground(brand.light, night = false), + bgRaised = Color(DdsLightColors.bgRaised), bgBrand = brand.light.bgBrand, bgBrandHover = brand.light.bgBrandHover, bgBrandSubtle = brand.light.bgBrandSubtle, diff --git a/android/dds-compose/src/test/kotlin/kr/devslab/dds/compose/theme/ColorSchemeTest.kt b/android/dds-compose/src/test/kotlin/kr/devslab/dds/compose/theme/ColorSchemeTest.kt index d74e7023..d8bdc025 100644 --- a/android/dds-compose/src/test/kotlin/kr/devslab/dds/compose/theme/ColorSchemeTest.kt +++ b/android/dds-compose/src/test/kotlin/kr/devslab/dds/compose/theme/ColorSchemeTest.kt @@ -2,6 +2,7 @@ package kr.devslab.dds.compose.theme import androidx.compose.material3.contentColorFor import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance import kr.devslab.dds.tokens.DdsDarkColors import kr.devslab.dds.tokens.DdsLightColors import org.junit.Assert.assertEquals @@ -128,6 +129,23 @@ class ColorSchemeTest { } } + /** + * The chosen segment's face (D-054): lighter than the track it sits on in both themes, so + * it reads as raised, never as a hole; and the label on it reads. + */ + @Test + fun aRaisedFaceIsLighterThanTheSubtleTrackAndCarriesText() { + for ((c, name) in listOf(light to "light", dark to "dark")) { + assertTrue("$name bgRaised is lighter than bgSubtle", c.bgRaised.luminance() > c.bgSubtle.luminance()) + for ((text, label) in listOf(c.textPrimary to "textPrimary", c.textSecondary to "textSecondary")) { + val ratio = contrastRatio(text, c.bgRaised) + assertTrue("$name $label on bgRaised is %.2f:1".format(ratio), ratio >= DdsMinTextContrast) + } + } + assertEquals("dark: zinc.800", Color(DdsDarkColors.bgRaised), dark.bgRaised) + assertEquals("light: the white of bg.default, as before", light.bgDefault, light.bgRaised) + } + /** A warm paper page: light enough for every system text color. */ private val paper = Color(red = 0.98f, green = 0.96f, blue = 0.92f) diff --git a/android/dds-tokens/src/main/kotlin/kr/devslab/dds/tokens/DdsColorTokens.kt b/android/dds-tokens/src/main/kotlin/kr/devslab/dds/tokens/DdsColorTokens.kt index 8ef50a5d..b51b78fa 100644 --- a/android/dds-tokens/src/main/kotlin/kr/devslab/dds/tokens/DdsColorTokens.kt +++ b/android/dds-tokens/src/main/kotlin/kr/devslab/dds/tokens/DdsColorTokens.kt @@ -6,6 +6,7 @@ object DdsLightColors { const val bgDefault: Long = 0xFFFFFFFF const val bgSubtle: Long = 0xFFFAFAFA const val bgElevated: Long = 0xFFFFFFFF + const val bgRaised: Long = 0xFFFFFFFF const val bgBrand: Long = 0xFF06B6D4 const val bgBrandHover: Long = 0xFF22D3EE const val bgBrandSubtle: Long = 0xFFECFEFF @@ -39,6 +40,7 @@ object DdsDarkColors { const val bgDefault: Long = 0xFF09090B const val bgSubtle: Long = 0xFF18181B const val bgElevated: Long = 0xFF18181B + const val bgRaised: Long = 0xFF27272A const val bgBrand: Long = 0xFF06B6D4 const val bgBrandHover: Long = 0xFF22D3EE const val bgBrandSubtle: Long = 0xFF164E63 diff --git a/android/dds-tokens/src/main/kotlin/kr/devslab/dds/tokens/DdsTokensInfo.kt b/android/dds-tokens/src/main/kotlin/kr/devslab/dds/tokens/DdsTokensInfo.kt index 28c6a0d7..adbf0186 100644 --- a/android/dds-tokens/src/main/kotlin/kr/devslab/dds/tokens/DdsTokensInfo.kt +++ b/android/dds-tokens/src/main/kotlin/kr/devslab/dds/tokens/DdsTokensInfo.kt @@ -6,5 +6,5 @@ object DdsTokensInfo { const val sourcePackage: String = "@devslab/dds-tokens" const val sourceRepository: String = "https://github.com/devslab-kr/dds" /** SHA-256 of the token files (tokens/ in the source repository) these values were generated from. */ - const val sourceSha256: String = "738637d53e5e35b63b8afe09e0c13e7171e38c70ecf1af528a757f44370a87b6" + const val sourceSha256: String = "c50240ca80ddc35fe54009f9203d8dadaff9ad051bd0ff171803222f2a2521b3" } diff --git a/android/dds-tokens/src/main/res/values-night/dds_tokens.xml b/android/dds-tokens/src/main/res/values-night/dds_tokens.xml index 59fa8e2f..3145fd9b 100644 --- a/android/dds-tokens/src/main/res/values-night/dds_tokens.xml +++ b/android/dds-tokens/src/main/res/values-night/dds_tokens.xml @@ -4,6 +4,7 @@ #FF09090B #FF18181B #FF18181B + #FF27272A #FF164E63 #FFFAFAFA #FFFAFAFA diff --git a/android/dds-tokens/src/main/res/values/dds_tokens.xml b/android/dds-tokens/src/main/res/values/dds_tokens.xml index 26cff995..c9f57b86 100644 --- a/android/dds-tokens/src/main/res/values/dds_tokens.xml +++ b/android/dds-tokens/src/main/res/values/dds_tokens.xml @@ -4,6 +4,7 @@ #FFFFFFFF #FFFAFAFA #FFFFFFFF + #FFFFFFFF #FF06B6D4 #FF22D3EE #FFECFEFF diff --git a/android/gradle.properties b/android/gradle.properties index 1482046f..72871498 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -8,4 +8,4 @@ kotlin.code.style=official # Android releases are versioned apart from the web packages (docs/decisions.md D-048). # The release workflow overrides VERSION_NAME from the android-v* tag. GROUP=kr.devslab -VERSION_NAME=0.8.0 +VERSION_NAME=0.8.1 diff --git a/docs/backlog.md b/docs/backlog.md index 20db2de7..d075d08f 100644 --- a/docs/backlog.md +++ b/docs/backlog.md @@ -336,7 +336,7 @@ D-048. 가이드 [`mobile-native.ko.md`](mobile-native.ko.md), 설계 (bring-into-view), 최소 줄 수(`minLines`). 그리고 단계 화면의 틀. - [x] **세그먼트 컨트롤(0.8.0, #149, 2026-11-13)** — `DdsSegmentedControl`·`DdsSegment`. Qooroo의 명함 종류 토글이 웹 SegmentedControl 모양을 골라서(소유자) 그 웹 규칙을 터치 크기 그대로 옮겼다. 가이드 §12, 변경 기록 0.8.0. - 소유자가 정할 것: 고른 면의 그림자(§10의 20번). 다음: Qooroo가 앱의 사본 `DdsStyleSegmented`를 이것으로 바꾼다. + 고른 면은 0.8.1에서 `bg.raised`(어두운 `zinc.800`), 그림자 1단계로 확정(D-054, §10의 20번). 다음: Qooroo가 앱의 사본 `DdsStyleSegmented`를 이것으로 바꾼다. - [x] **화면 틀(0.7.0) — 배포했다(#147, `android-v0.7.0`, 2026-11-13).** 정산나우가 로컬 빌드로 옮겨 배포 전에 확인했다. 앱의 모든 화면을 DDS 부품으로 옮기고, 475×751·704×933·933×704에서 밝은·어두운 화면으로(475×751은 글자 200%도) `DdsFrameCheck`와 `assertSameLines`로 쟀다 — 1dp 넘게 어긋난 시작선이 없었다. 펼친 폰의 전체 연결 테스트에서 DDS 탓의 실패는 없었다. 거기서도 확인하지 못한 것: 폰을 실제로 가로로 돌린 것, TalkBack, 접힌 폰의 전체 테스트. `DdsAppFrame`, 새 `DdsScaffold`, `DdsBodyColumn`·`ddsBody`·`ddsBodyLines`, 본문 선에 서는 `DdsTopBar` (`onBack`, 두 줄 제목), `DdsCenteredColumn`·`DdsCenteredTitle`, `DdsPaneList`·`DdsPaneItem`, 새 꾸러미 diff --git a/docs/decisions.md b/docs/decisions.md index 9cfb84dd..70943117 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -5,6 +5,47 @@ --- +## D-054 — 고른 세그먼트는 `bg.raised` 면에 그림자 1단계 (2026-11-13) + +**문제.** 세그먼트 컨트롤의 고른 칸은 `bg.default` 면이었다. 밝은 화면에서는 흰 면이 `bg.subtle` +트랙 위로 올라와 보이지만, 어두운 화면에서는 `bg.default`(`zinc.950` `#09090B`)가 트랙 +`bg.subtle`(`zinc.900` `#18181B`)보다 **어둡고** 그림자도 보이지 않아, 고른 칸이 구멍처럼 보였다. +Qooroo의 전체 화면 점검에서 실제로 지적됐다. 안드로이드 0.8.0과 웹 `SegmentedControl`이 같았다. +또 안드로이드 가이드 §10의 5번(카드·메뉴 그림자 없음)과 달리 이 면은 웹의 그림자를 그대로 두어 +§10의 20번으로 소유자 확인을 기다리고 있었다. + +**소유자 결정(2026-11-13, Qooroo 세션을 거쳐).** "아 그럼 그렇게 가자. 그림자는 너의 추천대로." +어두운 면의 색은 새 토큰으로 정했다(이 세션에서 고른 안). + +**결정.** + +1. **새 시맨틱 토큰 `color.bg.raised`.** 밝은 화면 `#ffffff`(전과 같은 흰 면), 어두운 화면 + `zinc.800`(`#27272A`) — `bg.subtle`보다 한 단계 밝다. 뜻은 "옅은 바탕 위로 올라온 면"이고 첫 + 쓰임은 고른 세그먼트다. 제품이 정하는 바탕 셋(D-053)에는 넣지 않는다 — 시스템 값이다. +2. **고른 세그먼트 면은 `bg.raised`**, 웹(`dds-css` → react·solid·vue)과 안드로이드 + (`DdsSegmentedControl`)가 함께 바뀐다. +3. **그림자는 1단계 그대로.** 2단계는 2dp 트랙 간격 밖으로 번져 떠 있는 카드처럼 보인다. 1dp + `border.strong` 선도 그대로다. 안드로이드 §10의 20번은 이것으로 확정이다. + +**대비.** `text.primary`(`zinc.50`) 위 `bg.raised` 어두운 값 14.27:1, `text.secondary` +(`zinc.400`) 5.81:1. 밝은 화면은 전과 같다. `tests/foundation-contracts.test.mjs`와 안드로이드 +`ColorSchemeTest`가 "두 테마 모두 `bg.raised`가 `bg.subtle`보다 밝고, 본문·보조 글자가 4.5:1 이상"을 +고정한다. + +**대안.** ① `border.default`(어두운 `zinc.800`)를 면에 칠한다 — 선 토큰을 면에 쓰는 것이고, 밝은 +화면에서는 `zinc.200` 회색 면이 되어 웹 모양이 바뀐다. ② 트랙 위에 `overlay.hover-tint`(흰 8%)를 +얹는다(약 `#2B2B2E`) — 덮개 토큰이라 hover한 다른 칸과 고른 칸이 똑같아진다. ③ `bg.elevated` — +어두운 값이 트랙과 같은 `zinc.900`이다. + +**영향.** 웹 패키지는 토큰이 하나 늘었다(추가만, 이름 변경·삭제 없음). 어두운 화면의 세그먼트 +컨트롤만 달라 보인다. 안드로이드는 0.8.1(`DdsColors.bgRaised` 추가). 이 토큰의 사본을 들고 있는 +소비자(asklinq `apps/api/src/dds/tokens.ts`)는 다음 동기화 때 받는다. + +**다시 볼 때.** `bg.raised`를 쓰는 두 번째 부품이 생길 때(토글 손잡이 등) — 뜻이 "고른 세그먼트"보다 +넓어지는지. + +--- + ## D-053 — 제품은 대비 예외를 적을 수 있고, 바탕 셋을 정한다 (2026-11-12) **문제.** Qooroo가 앱 모양을 다시 정하면서 DDS 안드로이드 0.5.0으로 할 수 없는 것이 나왔다. diff --git a/docs/design-system.ko.md b/docs/design-system.ko.md index e9347398..50ee7bf8 100644 --- a/docs/design-system.ko.md +++ b/docs/design-system.ko.md @@ -170,6 +170,7 @@ | `color.bg.default` | `#ffffff` | `zinc.950` | 기본 화면 배경 | | `color.bg.subtle` | `zinc.50` | `zinc.900` | 섹션·카드 구분 배경 | | `color.bg.elevated` | `#ffffff` + elevation | `zinc.900` | 모달·팝오버·시트 | +| `color.bg.raised` | `#ffffff` | `zinc.800` | 옅은 바탕 위로 올라온 면 — 세그먼트의 고른 칸. 다크에서 `bg.subtle`보다 한 단계 밝다 (D-054) | | `color.bg.brand` | `cyan.500` | `cyan.500` | 브랜드 강조 면 (주요 버튼 등) | | `color.bg.brand-hover` | `cyan.400` | `cyan.400` | 브랜드 면 hover (홈피 관용) | | `color.bg.brand-subtle` | `cyan.50` | `cyan.900` | 브랜드 옅은 배경 | diff --git a/docs/design-system.md b/docs/design-system.md index 20f05d72..dea595b5 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -178,6 +178,7 @@ toggle**; both mappings are first-class from day one. | `color.bg.default` | `#ffffff` | `zinc.950` | base screen background | | `color.bg.subtle` | `zinc.50` | `zinc.900` | section/card separation | | `color.bg.elevated` | `#ffffff` + elevation | `zinc.900` | modals, popovers, sheets | +| `color.bg.raised` | `#ffffff` | `zinc.800` | a face raised above the quiet background — the chosen segment. In dark one step lighter than `bg.subtle` (D-054) | | `color.bg.brand` | `cyan.500` | `cyan.500` | brand surfaces (primary buttons etc.) | | `color.bg.brand-hover` | `cyan.400` | `cyan.400` | brand surface hover (homepage idiom) | | `color.bg.brand-subtle` | `cyan.50` | `cyan.900` | soft brand backgrounds | diff --git a/docs/mobile-native.ko.md b/docs/mobile-native.ko.md index e3f51566..c38b9f69 100644 --- a/docs/mobile-native.ko.md +++ b/docs/mobile-native.ko.md @@ -761,7 +761,7 @@ Compose 벡터는 `dds-icons`의 SVG에서 **생성**한다(`node scripts/genera | 17 | 뒤로 가기와 앱 종료 | 이전 화면 → 직전 탭 순으로 돌아가고, 종료는 묻는다. 600ms 안에 두 번 누르면 묻는다 | §2.3.1. 정산나우의 동작 | | 18 | 탭 전환의 움직임 | 새 탭이 이전 탭 위로 나타남, 150ms. 밀기 없음(0.5.0, D-051). `DdsScreenMotion`만 쓰면 없음 | §2.3. 비운 자리 위에서는 같은 페이드가 실물 기기에서 깜빡임으로 읽혔다(0.3.0) | | 19 | 앞으로·뒤로 이동에서 화면을 흐리게 바꾸는가 | 내용: 아니다, 첫 프레임부터 불투명하고 밀기만. 화면: 그렇다, 이전 화면(또는 돌아갈 화면) 위에서 100ms(0.5.0, D-051) | §2.3의 18·20번. 빈 자리 위로 나타내면 실물 기기에서 빈 내용 자리가 보였다(0.3.0) | -| 20 | 고른 세그먼트 면의 그림자 | 웹대로 1단계(`DdsElevation.level1`) | 5번(카드 그림자 없음)은 카드·메뉴의 규칙이다. 세그먼트는 웹 그대로 두었고, 어두운 화면에서는 1dp 선이 구분한다. 0.8.0의 `DdsSegmentedControl` | +| 20 | 고른 세그먼트 면의 그림자 | **정했다(2026-11-13, D-054):** 1단계 그대로, 면은 `bg.raised` | 5번(카드 그림자 없음)은 카드·메뉴의 규칙이다. 2단계는 트랙 간격 밖으로 번진다. 어두운 화면에서는 그림자가 안 보이고 `bg.default` 면이 트랙보다 어두워 구멍처럼 보였다 → 트랙보다 밝은 `bg.raised`(`zinc.800`). 0.8.1, 웹도 함께 | **항목별 상태 (2026-11-10).** @@ -773,7 +773,7 @@ Compose 벡터는 `dds-icons`의 SVG에서 **생성**한다(`node scripts/genera 소유자가 확정하기 전까지는 확정된 규칙이 아니다. - **아직 소유자에게 묻지 않은 것:** 14, 15, 16번(0.2.0 부품과 함께 추가), 17번(0.3.0, 소유자가 정산나우를 폰에서 쓰며 한 말에서 나왔다 — 숫자는 제안값이다). - 20번(0.8.0의 세그먼트 컨트롤 — 웹 그대로 둔 값이다). +- **소유자가 고른 것(2026-11-13, D-054):** 20번 — 고른 세그먼트는 그림자 1단계, 면은 `bg.raised`. - **소유자 말에 따라 바꾼 것(2026-11-11, D-051):** 18·19번. 0.3.0과 0.4.0은 이 페이드를 뺐었다. 소유자는 움직임이 계속 사라지는 것을 원하지 않았고, 비는 프레임이 없는 방식으로 되살렸다. 시간은 0.2.0의 값이다. @@ -882,7 +882,7 @@ Compose 벡터는 `dds-icons`의 SVG에서 **생성**한다(`node scripts/genera | 부품 | 고정해 주는 것 | 웹 | | --- | --- | --- | -| `DdsSegmentedControl`, `DdsSegment` | 몇 개 안 되는 보기 중 하나 고르기(보기 전환, 필터, 개인 / 비즈니스 같은 종류). 트랙은 `bg.subtle` + 1dp `border.strong` 선 + 둥글기 8, 안쪽 2dp, 칸 사이 2dp. 칸은 둥글기 4, `label` 글자, `text.secondary`. 고른 칸은 `bg.default` 면 + 1dp `border.strong` 선 + 1단계 그림자 + `text.primary` + 굵기 600. 높이는 하나(터치 높이): 트랙 50 = 터치 48 + 위아래 선 1, 칸의 면 44, 큰 글자 모드는 터치 56. 라벨은 줄바꿈하지 않는다 | SegmentedControl | +| `DdsSegmentedControl`, `DdsSegment` | 몇 개 안 되는 보기 중 하나 고르기(보기 전환, 필터, 개인 / 비즈니스 같은 종류). 트랙은 `bg.subtle` + 1dp `border.strong` 선 + 둥글기 8, 안쪽 2dp, 칸 사이 2dp. 칸은 둥글기 4, `label` 글자, `text.secondary`. 고른 칸은 `bg.raised` 면(밝은 화면 흰색, 어두운 화면 `zinc.800` — 0.8.1부터) + 1dp `border.strong` 선 + 1단계 그림자 + `text.primary` + 굵기 600. 높이는 하나(터치 높이): 트랙 50 = 터치 48 + 위아래 선 1, 칸의 면 44, 큰 글자 모드는 터치 56. 라벨은 줄바꿈하지 않는다 | SegmentedControl | - **고르는 것이 아무것도 밀지 않는다.** 기본(내용 폭)에서는 칸마다 라벨이 굵은 글자의 폭을 늘 차지하고 있어서, 다른 칸을 골라도 이웃 칸이 움직이지 않는다. 칸이 너무 많으면 트랙 @@ -901,9 +901,10 @@ Compose 벡터는 `dds-icons`의 SVG에서 **생성**한다(`node scripts/genera false)`)은 그 칸만 0.45로 그린다. 누름은 물결이 아니라 면의 옅은 덮개, 마우스를 올리면 웹처럼 hover 덮개다. 포커스(키보드·D패드)는 칸 안쪽 2dp `border.focus` 선이 고른 칸의 선을 대신한다. -- **고른 칸의 그림자는 웹대로 둔다.** 카드·메뉴의 그림자는 뺐지만(§10의 5번) 이 면은 웹 그대로 - 1단계 그림자를 쓴다. 어두운 화면에서는 그림자가 거의 보이지 않아도 1dp 선이 면을 구분한다. - 소유자가 확정할 항목이다(§10의 20번). +- **고른 칸은 트랙보다 밝은 면이다(0.8.1, D-054).** 카드·메뉴의 그림자는 뺐지만(§10의 5번) 이 + 면은 웹 그대로 1단계 그림자를 쓴다. 어두운 화면에서는 그림자가 보이지 않으므로 면 자체가 트랙 + (`bg.subtle` `zinc.900`)보다 한 단계 밝은 `bg.raised`(`zinc.800`)다. 0.8.0은 `bg.default` + (`zinc.950`)라 트랙보다 어두워 구멍처럼 보였다. 밝은 화면은 그대로 흰 면이다. - `segments`는 하나 이상이어야 하고 값이 겹치면 안 된다(어기면 예외). `selected`가 어느 칸의 값도 아니면 아무 칸도 고르지 않은 상태다. `onSelect`는 이미 고른 칸을 누르거나 `readOnly`일 때 부르지 않는다. diff --git a/docs/mobile-native.md b/docs/mobile-native.md index 6035031d..22b58635 100644 --- a/docs/mobile-native.md +++ b/docs/mobile-native.md @@ -795,7 +795,7 @@ in for now"; making them DDS rules is the owner's call. Once decided, | 17 | Back and leaving the app | back to the screen, then the previous tab; ask before leaving; two presses within 600ms ask | §2.3.1. JeongsanNow's behavior | | 18 | Motion on a tab switch | the new tab appears over the one before, 150ms; nothing slides (0.5.0, D-051). Nothing with `DdsScreenMotion` alone | §2.3. Over an emptied page the same fade read as a flicker on a device (0.3.0) | | 19 | Whether a screen fades on push and pop | the content: no, opaque from the first frame, it only slides. The screen: yes, 100ms over the screen before, or the one returned to (0.5.0, D-051) | §2.3, rules 18 and 20. Fading in over nothing showed an empty content area on a device (0.3.0) | -| 20 | The shadow under the chosen segment's face | as on the web, level 1 (`DdsElevation.level1`) | No. 5 (no card shadow) is the rule for cards and menus. The segment is left as the web draws it; in the dark theme the 1dp edge sets it apart. 0.8.0's `DdsSegmentedControl` | +| 20 | The shadow under the chosen segment's face | **Settled (2026-11-13, D-054):** level 1 as on the web, on a `bg.raised` face | No. 5 (no card shadow) is the rule for cards and menus. Level 2 spreads past the track's gap. In the dark theme the shadow does not show and the `bg.default` face was darker than the track, so it read as a hole → `bg.raised` (`zinc.800`), lighter than the track. 0.8.1, with the web | **Where each item stands (2026-11-10).** @@ -807,7 +807,7 @@ in for now"; making them DDS rules is the owner's call. Once decided, dialog". These are not confirmed rules until the owner says so. - **Not yet put to the owner:** 14, 15, 16 (added with the 0.2.0 components); 17 (0.3.0, from the owner's own words while using JeongsanNow on a phone — the numbers are proposals). - 20 (0.8.0's segmented control — the value is the web's, left as it is). +- **Chosen by the owner (2026-11-13, D-054):** 20 — the chosen segment keeps a level-1 shadow on a `bg.raised` face. - **Changed on the owner's word (2026-11-11, D-051):** 18 and 19. 0.3.0 and 0.4.0 had taken these fades out; the owner did not want movement to keep disappearing, so they are back, done the way that leaves no empty frame. The durations are the 0.2.0 ones. @@ -923,7 +923,7 @@ end-aligned menu). | Component | What it settles | Web | | --- | --- | --- | -| `DdsSegmentedControl`, `DdsSegment` | One choice among a few visible options (a view switch, a filter, a type such as Person / Business). The track is `bg.subtle` with a 1dp `border.strong` edge and radius 8, 2dp inside it and 2dp between segments. A segment has radius 4, the `label` type and `text.secondary`. The chosen one is a `bg.default` face with a 1dp `border.strong` edge, a level-1 shadow, `text.primary` and weight 600. One height, the touch height: the track is 50 = 48 of touch + the 1dp edge above and below, a segment's face is 44, and in large text mode the touch height is 56. Labels never wrap | SegmentedControl | +| `DdsSegmentedControl`, `DdsSegment` | One choice among a few visible options (a view switch, a filter, a type such as Person / Business). The track is `bg.subtle` with a 1dp `border.strong` edge and radius 8, 2dp inside it and 2dp between segments. A segment has radius 4, the `label` type and `text.secondary`. The chosen one is a `bg.raised` face (white in light, `zinc.800` in dark — from 0.8.1) with a 1dp `border.strong` edge, a level-1 shadow, `text.primary` and weight 600. One height, the touch height: the track is 50 = 48 of touch + the 1dp edge above and below, a segment's face is 44, and in large text mode the touch height is 56. Labels never wrap | SegmentedControl | - **Choosing moves nothing.** By default (content width) every label takes the width of its bold form in every state, so choosing another segment never moves its neighbours. Too many @@ -946,9 +946,11 @@ end-aligned menu). 0.45 alone. A press is a pale tint on the face, not a ripple; a mouse over it shows the hover tint as on the web. Focus (keyboard, D-pad) is a 2dp `border.focus` line inside the segment that takes the place of the chosen face's edge. -- **The chosen face keeps the web's shadow.** Cards and menus dropped theirs (§10 no. 5), but - this face uses the web's level-1 shadow. In the dark theme, where a shadow hardly shows, the - 1dp edge sets the face apart. It is for the owner to settle (§10 no. 20). +- **The chosen face is lighter than the track (0.8.1, D-054).** Cards and menus dropped their + shadow (§10 no. 5), but this face keeps the web's level-1 shadow. In the dark theme, where the + shadow does not show, the face itself is `bg.raised` (`zinc.800`), one step lighter than the + track (`bg.subtle`, `zinc.900`). In 0.8.0 it was `bg.default` (`zinc.950`), darker than the + track, and read as a hole. The light theme is unchanged: a white face. - `segments` must not be empty and their values must not repeat (either throws). A `selected` that is none of the values chooses none. `onSelect` is not called for the segment already chosen, nor while `readOnly`. diff --git a/packages/dds-css/src/segmented-control.css b/packages/dds-css/src/segmented-control.css index 82e4794e..997600a6 100644 --- a/packages/dds-css/src/segmented-control.css +++ b/packages/dds-css/src/segmented-control.css @@ -116,7 +116,9 @@ .dds-segmented__item[data-state="checked"] { border-color: var(--dds-color-border-strong); - background: var(--dds-color-bg-default); + /* bg.raised, not bg.default: in dark, bg.default is darker than the track + * and the chosen face read as a hole (D-054). */ + background: var(--dds-color-bg-raised); box-shadow: var(--dds-elevation-1); color: var(--dds-color-text-primary); font-weight: 600; diff --git a/preview/index.html b/preview/index.html index 5093ff1b..6a9681fc 100644 --- a/preview/index.html +++ b/preview/index.html @@ -705,6 +705,7 @@
자료를 다시 색인할까요?
const sem = [ ["color.bg.default","#ffffff","#09090b","기본 화면 배경"], ["color.bg.subtle","#fafafa","#18181b","섹션·카드 구분 배경"], + ["color.bg.raised","#ffffff","#27272a","옅은 바탕 위로 올라온 면 (세그먼트의 고른 칸)"], ["color.bg.brand","#06b6d4","#06b6d4","브랜드 강조 면"], ["color.bg.brand-hover","#22d3ee","#22d3ee","브랜드 면 hover"], ["color.bg.brand-subtle","#ecfeff","#164e63","브랜드 옅은 배경"], diff --git a/tests/foundation-contracts.test.mjs b/tests/foundation-contracts.test.mjs index d0b188bd..e120765b 100644 --- a/tests/foundation-contracts.test.mjs +++ b/tests/foundation-contracts.test.mjs @@ -413,3 +413,33 @@ test("form controls draw their resting edge in border.control, not border.strong assert.match(segmented, /\.dds-segmented__track\s*\{[^}]*border:\s*1px solid var\(--dds-color-border-control, var\(--dds-color-border-strong\)\)/); assert.doesNotMatch(segmented, /var\(--dds-color-border-control\)/); }); + +test("bg.raised is lighter than bg.subtle and text reads on it, and the chosen segment uses it (D-054)", async () => { + const palette = JSON.parse(await read("tokens/palette.json")); + const hex = (reference) => { + if (!reference.startsWith("{")) return reference; + let node = palette; + for (const segment of reference.slice(1, -1).split(".")) node = node[segment]; + return node.$value; + }; + const luminance = (value) => { + const [r, g, b] = [1, 3, 5].map((i) => parseInt(value.slice(i, i + 2), 16) / 255) + .map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4)); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; + }; + const contrast = (a, b) => { + const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x); + return (hi + 0.05) / (lo + 0.05); + }; + for (const theme of ["light", "dark"]) { + const { bg, text } = JSON.parse(await read(`tokens/semantic.${theme}.json`)).color; + const raised = hex(bg.raised.$value); + assert.ok(luminance(raised) > luminance(hex(bg.subtle.$value)), `${theme} bg.raised must be lighter than bg.subtle`); + for (const name of ["primary", "secondary"]) { + const ratio = contrast(hex(text[name].$value), raised); + assert.ok(ratio >= 4.5, `${theme} text.${name} on bg.raised is ${ratio.toFixed(2)}:1`); + } + } + const segmented = await read("packages/dds-css/src/segmented-control.css"); + assert.match(segmented, /\.dds-segmented__item\[data-state="checked"\]\s*\{[^}]*background:\s*var\(--dds-color-bg-raised\)/); +}); diff --git a/tokens/semantic.dark.json b/tokens/semantic.dark.json index 188ede66..0c88975c 100644 --- a/tokens/semantic.dark.json +++ b/tokens/semantic.dark.json @@ -5,6 +5,7 @@ "default": { "$value": "{palette.zinc.950}", "$type": "color" }, "subtle": { "$value": "{palette.zinc.900}", "$type": "color" }, "elevated": { "$value": "{palette.zinc.900}", "$type": "color" }, + "raised": { "$value": "{palette.zinc.800}", "$type": "color" }, "brand": { "$value": "{palette.cyan.500}", "$type": "color" }, "brand-hover": { "$value": "{palette.cyan.400}", "$type": "color" }, "brand-subtle": { "$value": "{palette.cyan.900}", "$type": "color" }, diff --git a/tokens/semantic.light.json b/tokens/semantic.light.json index 47ad9a94..dcc995cd 100644 --- a/tokens/semantic.light.json +++ b/tokens/semantic.light.json @@ -5,6 +5,7 @@ "default": { "$value": "{palette.white}", "$type": "color" }, "subtle": { "$value": "{palette.zinc.50}", "$type": "color" }, "elevated": { "$value": "{palette.white}", "$type": "color" }, + "raised": { "$value": "{palette.white}", "$type": "color" }, "brand": { "$value": "{palette.cyan.500}", "$type": "color" }, "brand-hover": { "$value": "{palette.cyan.400}", "$type": "color" }, "brand-subtle": { "$value": "{palette.cyan.50}", "$type": "color" },