From bafb3ccb01b457fc3cae659b7a0a4d7bd2553b5e Mon Sep 17 00:00:00 2001 From: Sin-Kang Date: Fri, 13 Nov 2026 18:42:54 +0900 Subject: [PATCH] feat(android): an app can pass its own typeface to DdsTheme (0.9.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner-approved: Qooroo owns its font (Pretendard Std Variable), DDS keeps Pretendard as the default. DdsTheme(fontFamily = DdsSans) passes the family into the type scale, the only place DDS sets one, so DDS components and Material text are all set in it. Sizes, line heights and weights stay the tokens'. The default is the same DdsSans object, so an app that passes nothing changes not one glyph. No per-locale mapper: an app swaps the family it passes when the language changes, which keeps the API to one parameter. Tests: JVM — every style is DdsSans by default, a passed family reaches all sixteen styles and Material's type, and nothing but the family changes. Device — a DdsButton and a DdsSegmentedControl lay their labels out in the passed family, and in DdsSans without one. --- README.ko.md | 2 +- README.md | 2 +- android/CHANGELOG.ko.md | 8 +++ android/CHANGELOG.md | 8 +++ android/README.ko.md | 4 +- android/README.md | 4 +- .../dds/compose/ThemeFontOnDeviceTest.kt | 64 +++++++++++++++++++ .../kr/devslab/dds/compose/theme/DdsTheme.kt | 11 +++- .../dds/compose/theme/DdsTypography.kt | 42 ++++++------ .../dds/compose/theme/TypographyFamilyTest.kt | 45 +++++++++++++ android/gradle.properties | 2 +- docs/mobile-native.ko.md | 5 ++ docs/mobile-native.md | 6 ++ 13 files changed, 176 insertions(+), 27 deletions(-) create mode 100644 android/dds-compose/src/androidTest/kotlin/kr/devslab/dds/compose/ThemeFontOnDeviceTest.kt create mode 100644 android/dds-compose/src/test/kotlin/kr/devslab/dds/compose/theme/TypographyFamilyTest.kt diff --git a/README.ko.md b/README.ko.md index f6eab3ab..54e5b46a 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`이 생겼고, 0.8.1에서 어두운 화면의 고른 칸이 밝아졌고(`bg.raised`, D-054), 0.9.0에서 고른 면이 미끄러져 옮겨 가고(D-055), 0.9.1에서 앱이 그 속도를 정할 수 있다. 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), 0.9.0에서 고른 면이 미끄러져 옮겨 가고(D-055), 0.9.1에서 앱이 그 속도를, 0.9.2에서 `DdsTheme`에 자기 글꼴을 넘길 수 있다. RN·Ionic 패키지는 그 제품이 생길 때. ## 입력·선택·공유 달력 확장 diff --git a/README.md b/README.md index 03184428..f3009afe 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`, 0.8.1 its lighter chosen face in dark (`bg.raised`, D-054), 0.9.0 makes that face slide between choices (D-055), and 0.9.1 lets an app set the slide speed. 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`, 0.8.1 its lighter chosen face in dark (`bg.raised`, D-054), 0.9.0 makes that face slide between choices (D-055), 0.9.1 lets an app set the slide speed, and 0.9.2 lets an app pass its own typeface to `DdsTheme`. 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 01979734..6534390a 100644 --- a/android/CHANGELOG.ko.md +++ b/android/CHANGELOG.ko.md @@ -5,6 +5,14 @@ `kr.devslab:dds-android-tokens`, `kr.devslab:dds-compose`, 그리고 0.7.0부터 `kr.devslab:dds-compose-testing`은 한 버전을 쓰고, 웹 패키지와는 버전이 따로다(D-048). 릴리스는 `android-v<버전>` 태그로 낸다. +## 0.9.2 — 2026-11-13 + +**새로 생긴 것** + +- **`DdsTheme(fontFamily = …)`** — 앱이 자기 글꼴을 넘기면 DDS 부품과 Material 글자가 모두 그 글꼴로 그려진다. + 기본값은 `DdsSans`(Pretendard)로 지금과 같아서, 넘기지 않은 앱은 한 글자도 바뀌지 않는다. 크기·줄높이·굵기는 DDS + 글자 표 그대로이고 글꼴만 바뀐다. 가이드 §4. + ## 0.9.1 — 2026-11-13 **새로 생긴 것** diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 53734ee0..abcc5c9e 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -5,6 +5,14 @@ `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.9.2 — 2026-11-13 + +**New** + +- **`DdsTheme(fontFamily = …)`** — an app that passes its own family gets DDS components and Material text set in it. + The default is `DdsSans` (Pretendard), as today, so an app that passes nothing changes not one glyph. Only the family + changes; sizes, line heights and weights stay the DDS type scale. Guide §4. + ## 0.9.1 — 2026-11-13 **New** diff --git a/android/README.ko.md b/android/README.ko.md index 034e762d..c72200eb 100644 --- a/android/README.ko.md +++ b/android/README.ko.md @@ -26,8 +26,8 @@ dependencies { ```kotlin dependencies { - implementation("kr.devslab:dds-compose:0.9.1") - androidTestImplementation("kr.devslab:dds-compose-testing:0.9.1") + implementation("kr.devslab:dds-compose:0.9.2") + androidTestImplementation("kr.devslab:dds-compose-testing:0.9.2") } ``` diff --git a/android/README.md b/android/README.md index bc05aca0..6f1a6db5 100644 --- a/android/README.md +++ b/android/README.md @@ -26,8 +26,8 @@ dependencies { ```kotlin dependencies { - implementation("kr.devslab:dds-compose:0.9.1") - androidTestImplementation("kr.devslab:dds-compose-testing:0.9.1") + implementation("kr.devslab:dds-compose:0.9.2") + androidTestImplementation("kr.devslab:dds-compose-testing:0.9.2") } ``` diff --git a/android/dds-compose/src/androidTest/kotlin/kr/devslab/dds/compose/ThemeFontOnDeviceTest.kt b/android/dds-compose/src/androidTest/kotlin/kr/devslab/dds/compose/ThemeFontOnDeviceTest.kt new file mode 100644 index 00000000..e8a23467 --- /dev/null +++ b/android/dds-compose/src/androidTest/kotlin/kr/devslab/dds/compose/ThemeFontOnDeviceTest.kt @@ -0,0 +1,64 @@ +package kr.devslab.dds.compose + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.semantics.SemanticsActions +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.text.TextLayoutResult +import androidx.compose.ui.text.font.FontFamily +import kr.devslab.dds.compose.component.DdsButton +import kr.devslab.dds.compose.component.DdsSegment +import kr.devslab.dds.compose.component.DdsSegmentedControl +import kr.devslab.dds.compose.theme.DdsSans +import kr.devslab.dds.compose.theme.DdsTheme +import org.junit.Assert.assertSame +import org.junit.Rule +import org.junit.Test + +/** + * The font family an app passes to [DdsTheme] reaches the text DDS components lay out; + * without one, the text is Pretendard as before. + * + * Run on an emulator or a phone (CI has neither): + * ANDROID_SERIAL= ./gradlew :dds-compose:connectedDebugAndroidTest + */ +class ThemeFontOnDeviceTest { + @get:Rule + val rule = createComposeRule() + + private fun show(family: FontFamily?) { + val content: @Composable () -> Unit = { + Column { + DdsButton("저장", onClick = {}) + DdsSegmentedControl(listOf(DdsSegment(1, "개인"), DdsSegment(2, "비즈니스")), 1, {}, contentDescription = "종류") + } + } + rule.setContent { if (family == null) DdsTheme(content = content) else DdsTheme(fontFamily = family, content = content) } + rule.waitForIdle() + } + + /** The family the label was laid out in. */ + private fun familyOf(text: String): FontFamily? { + val results = mutableListOf() + rule.onNodeWithText(text, useUnmergedTree = true).fetchSemanticsNode() + .config[SemanticsActions.GetTextLayoutResult].action!!.invoke(results) + return results.single().layoutInput.style.fontFamily + } + + @Test + fun withoutAFamilyTheTextIsPretendard() { + show(null) + assertSame(DdsSans, familyOf("저장")) + assertSame(DdsSans, familyOf("개인")) + } + + @Test + fun aFamilyTheAppPassesIsTheOneTheTextIsLaidOutIn() { + val own = FontFamily.Serif + show(own) + assertSame(own, familyOf("저장")) + assertSame(own, familyOf("개인")) + assertSame(own, familyOf("비즈니스")) + } +} diff --git a/android/dds-compose/src/main/kotlin/kr/devslab/dds/compose/theme/DdsTheme.kt b/android/dds-compose/src/main/kotlin/kr/devslab/dds/compose/theme/DdsTheme.kt index a27e3045..a76b5e63 100644 --- a/android/dds-compose/src/main/kotlin/kr/devslab/dds/compose/theme/DdsTheme.kt +++ b/android/dds-compose/src/main/kotlin/kr/devslab/dds/compose/theme/DdsTheme.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.remember import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import kr.devslab.dds.tokens.DdsTouch @@ -29,16 +30,24 @@ import kr.devslab.dds.tokens.DdsTouch * DevsLab cyan on the system's backgrounds * @param largeText the app-offered large text mode: a stepped-up type scale * and a larger minimum touch target. It stacks with the system font scale. + * @param fontFamily the family every DDS style, and Material's type, is set in. + * [DdsSans] (Pretendard) by default; an app that bundles its own (another cut + * of Pretendard, a variable font) passes it here. Only the family changes: + * sizes, line heights and weights stay the DDS type scale. The app checks its + * family covers the weights the scale uses (400, 500, 600, 700) and the + * scripts it shows; to use another family for another language, pass a + * different family when that language is on */ @Composable public fun DdsTheme( darkTheme: Boolean = isSystemInDarkTheme(), brand: DdsBrand = rememberDdsBrand(), largeText: Boolean = false, + fontFamily: FontFamily = DdsSans, content: @Composable () -> Unit, ) { val colors = remember(darkTheme, brand) { ddsColors(darkTheme, brand) } - val typography = remember(largeText) { ddsTypography(largeText) } + val typography = remember(largeText, fontFamily) { ddsTypography(largeText, fontFamily) } val colorScheme = remember(colors, darkTheme, brand) { ddsColorScheme(colors, darkTheme, inverseTextBrand = (if (darkTheme) brand.light else brand.dark).textBrand) } diff --git a/android/dds-compose/src/main/kotlin/kr/devslab/dds/compose/theme/DdsTypography.kt b/android/dds-compose/src/main/kotlin/kr/devslab/dds/compose/theme/DdsTypography.kt index 919c7660..532e59ee 100644 --- a/android/dds-compose/src/main/kotlin/kr/devslab/dds/compose/theme/DdsTypography.kt +++ b/android/dds-compose/src/main/kotlin/kr/devslab/dds/compose/theme/DdsTypography.kt @@ -65,8 +65,8 @@ public class DdsTypography internal constructor( public val caption: TextStyle, ) -private fun DdsTypeToken.style(): TextStyle = TextStyle( - fontFamily = DdsSans, +private fun DdsTypeToken.style(family: FontFamily): TextStyle = TextStyle( + fontFamily = family, fontSize = fontSize.sp, lineHeight = lineHeight.sp, fontWeight = FontWeight(fontWeight), @@ -77,28 +77,32 @@ private fun DdsTypeToken.style(): TextStyle = TextStyle( lineHeightStyle = LineHeightStyle(LineHeightStyle.Alignment.Center, LineHeightStyle.Trim.None), ) -internal fun ddsTypography(largeText: Boolean): DdsTypography = +/** + * The eight styles in [family] ([DdsSans] unless the app passed its own to [DdsTheme]). + * Sizes, line heights and weights are the tokens' whatever the family. + */ +internal fun ddsTypography(largeText: Boolean, family: FontFamily = DdsSans): DdsTypography = if (largeText) { DdsTypography( - display = DdsLargeType.display.style(), - title1 = DdsLargeType.title1.style(), - title2 = DdsLargeType.title2.style(), - title3 = DdsLargeType.title3.style(), - body1 = DdsLargeType.body1.style(), - body2 = DdsLargeType.body2.style(), - label = DdsLargeType.label.style(), - caption = DdsLargeType.caption.style(), + display = DdsLargeType.display.style(family), + title1 = DdsLargeType.title1.style(family), + title2 = DdsLargeType.title2.style(family), + title3 = DdsLargeType.title3.style(family), + body1 = DdsLargeType.body1.style(family), + body2 = DdsLargeType.body2.style(family), + label = DdsLargeType.label.style(family), + caption = DdsLargeType.caption.style(family), ) } else { DdsTypography( - display = DdsType.display.style(), - title1 = DdsType.title1.style(), - title2 = DdsType.title2.style(), - title3 = DdsType.title3.style(), - body1 = DdsType.body1.style(), - body2 = DdsType.body2.style(), - label = DdsType.label.style(), - caption = DdsType.caption.style(), + display = DdsType.display.style(family), + title1 = DdsType.title1.style(family), + title2 = DdsType.title2.style(family), + title3 = DdsType.title3.style(family), + body1 = DdsType.body1.style(family), + body2 = DdsType.body2.style(family), + label = DdsType.label.style(family), + caption = DdsType.caption.style(family), ) } diff --git a/android/dds-compose/src/test/kotlin/kr/devslab/dds/compose/theme/TypographyFamilyTest.kt b/android/dds-compose/src/test/kotlin/kr/devslab/dds/compose/theme/TypographyFamilyTest.kt new file mode 100644 index 00000000..add55fe7 --- /dev/null +++ b/android/dds-compose/src/test/kotlin/kr/devslab/dds/compose/theme/TypographyFamilyTest.kt @@ -0,0 +1,45 @@ +package kr.devslab.dds.compose.theme + +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import org.junit.Assert.assertEquals +import org.junit.Assert.assertSame +import org.junit.Test + +/** The family an app passes to DdsTheme (0.9.2): the default is unchanged, a passed one is used everywhere. */ +class TypographyFamilyTest { + private fun DdsTypography.all(): List = listOf(display, title1, title2, title3, body1, body2, label, caption) + + @Test + fun byDefaultEveryStyleIsPretendard() { + for (large in listOf(false, true)) { + for (style in ddsTypography(large).all()) assertSame(DdsSans, style.fontFamily) + } + } + + @Test + fun aPassedFamilyIsUsedByEveryStyle() { + val own = FontFamily.Serif + for (large in listOf(false, true)) { + for (style in ddsTypography(large, own).all()) assertSame(own, style.fontFamily) + } + } + + @Test + fun onlyTheFamilyChanges() { + for (large in listOf(false, true)) { + val dds = ddsTypography(large).all() + val own = ddsTypography(large, FontFamily.Serif).all() + for ((a, b) in dds.zip(own)) assertEquals(a.copy(fontFamily = null), b.copy(fontFamily = null)) + } + } + + @Test + fun materialTypeFollowsTheFamily() { + val own = FontFamily.Serif + val material = materialTypography(ddsTypography(false, own)) + for (style in listOf(material.bodyLarge, material.labelLarge, material.titleLarge, material.displayLarge)) { + assertSame(own, style.fontFamily) + } + } +} diff --git a/android/gradle.properties b/android/gradle.properties index 9b7e64d4..f1bbb71a 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.9.1 +VERSION_NAME=0.9.2 diff --git a/docs/mobile-native.ko.md b/docs/mobile-native.ko.md index 2e5e8bf7..08aac930 100644 --- a/docs/mobile-native.ko.md +++ b/docs/mobile-native.ko.md @@ -627,6 +627,11 @@ Material3의 `primary`는 채움 면과 글자색을 겸하는데 DDS는 둘을 - 한글은 띄어쓰기에서만 줄을 바꾼다(`"…".keepWords()`) — 웹의 `keep-all`과 같은 결과. - 단위는 sp다(웹의 px·RN의 pt와 같은 숫자). 시스템 글자 배율을 그대로 따른다. - 모바일에만 있는 것: 글꼴은 Pretendard 하나(§9), Material3 칸 대응, 큰 글자 모드(§10의 7번). +- **제품이 자기 글꼴을 쓸 수 있다(0.9.2).** `DdsTheme(fontFamily = …)`에 넘기면 DDS 부품과 Material 글자가 모두 그 + 글꼴로 그려진다. 기본값은 지금처럼 `DdsSans`(Pretendard)이고, 넘기지 않은 앱은 바뀌는 것이 없다. 바뀌는 것은 + 글꼴뿐이고 크기·줄높이·굵기는 위 표 그대로다. 그 글꼴이 표의 굵기(400, 500, 600, 700)와 화면에 나오는 문자를 갖췄는지는 + 앱이 확인한다. 언어마다 다른 글꼴을 쓰려면 그 언어일 때 다른 글꼴을 넘긴다. Qooroo가 첫 사용처다(Pretendard Std + Variable). ## 5. 간격·둥글기·높이 diff --git a/docs/mobile-native.md b/docs/mobile-native.md index d8af8779..aa98db39 100644 --- a/docs/mobile-native.md +++ b/docs/mobile-native.md @@ -654,6 +654,12 @@ Material3's `primary` is both a fill and a text color, where DDS has `bg.brand` - Korean breaks only at spaces (`"…".keepWords()`) — the result of the web's `keep-all`. - Units are sp (the same numbers as the web's px and RN's pt), so the system font scale applies. - Mobile only: one typeface, Pretendard (§9); the Material3 slot mapping; large text mode (§10, item 7). +- **A product can use its own typeface (0.9.2).** Pass it to `DdsTheme(fontFamily = …)` and DDS + components and Material text are all set in it. The default stays `DdsSans` (Pretendard); an app + that passes nothing sees no change. Only the family changes: sizes, line heights and weights stay + the table above. The app checks its family has the weights the table uses (400, 500, 600, 700) + and the scripts it shows. For another family per language, pass a different family when that + language is on. Qooroo is the first user (Pretendard Std Variable). ## 5. Spacing, radius, heights