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