Updated on 2026-08-14
This commit is contained in:
parent
d30f231520
commit
40a5e13ad4
13 changed files with 2802 additions and 0 deletions
|
|
@ -1,9 +1,71 @@
|
|||
import java.security.MessageDigest
|
||||
|
||||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that generated Kotlin token files match the current ds-tokens submodule.
|
||||
* If this fails, run: cd core/ui/token-gen && npm run build
|
||||
*/
|
||||
abstract class VerifyDesignTokensTask : DefaultTask() {
|
||||
|
||||
@get:InputDirectory
|
||||
@get:PathSensitive(PathSensitivity.RELATIVE)
|
||||
abstract val tokensDir: DirectoryProperty
|
||||
|
||||
@get:InputFile
|
||||
@get:PathSensitive(PathSensitivity.NONE)
|
||||
abstract val hashFile: RegularFileProperty
|
||||
|
||||
@get:OutputFile
|
||||
abstract val stampFile: RegularFileProperty
|
||||
|
||||
@TaskAction
|
||||
fun verify() {
|
||||
val hashFileValue = hashFile.get().asFile
|
||||
require(hashFileValue.exists()) {
|
||||
"Design tokens hash file not found: ${hashFileValue.absolutePath}\n" +
|
||||
"Run the token generator: cd core/ui/token-gen && npm run build"
|
||||
}
|
||||
|
||||
val tokensDirValue = tokensDir.get().asFile
|
||||
require(tokensDirValue.exists() && tokensDirValue.isDirectory) {
|
||||
"ds-tokens submodule not found: ${tokensDirValue.absolutePath}\n" +
|
||||
"Run: git submodule update --init --recursive"
|
||||
}
|
||||
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
val jsonFiles = tokensDirValue.walkTopDown()
|
||||
.filter { it.isFile && it.extension == "json" }
|
||||
.sortedBy { it.relativeTo(tokensDirValue).path }
|
||||
.toList()
|
||||
|
||||
val nul = byteArrayOf(0)
|
||||
for (file in jsonFiles) {
|
||||
digest.update(file.relativeTo(tokensDirValue).invariantSeparatorsPath.toByteArray())
|
||||
digest.update(nul)
|
||||
digest.update(file.readBytes())
|
||||
digest.update(nul)
|
||||
}
|
||||
|
||||
val actual = digest.digest()
|
||||
.joinToString("") { b: Byte -> b.toInt().and(0xFF).toString(16).padStart(2, '0') }
|
||||
val expected = hashFileValue.readText().trim()
|
||||
|
||||
require(actual == expected) {
|
||||
"Design tokens are out of date!\n" +
|
||||
" ds-tokens hash: $actual\n" +
|
||||
" generated hash: $expected\n" +
|
||||
"Run the token generator: cd core/ui/token-gen && npm run build"
|
||||
}
|
||||
|
||||
stampFile.get().asFile.writeText(actual)
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.tangem.core.ui"
|
||||
|
||||
|
|
@ -15,6 +77,16 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
val verifyDesignTokens = tasks.register<VerifyDesignTokensTask>("verifyDesignTokens") {
|
||||
tokensDir.set(file("ds-tokens/tokens"))
|
||||
hashFile.set(file("src/main/java/com/tangem/core/ui/res/generated/.tokens-hash"))
|
||||
stampFile.set(layout.buildDirectory.file("tokens-verified.stamp"))
|
||||
}
|
||||
|
||||
tasks.named("preBuild") {
|
||||
dependsOn(verifyDesignTokens)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
/** Project - Domain */
|
||||
implementation(projects.domain.appTheme.models)
|
||||
|
|
|
|||
1
core/ui/ds-tokens
Submodule
1
core/ui/ds-tokens
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit f4d2156236d7f77b61f26250351f6bfa72262a69
|
||||
|
|
@ -0,0 +1 @@
|
|||
ddef7c44794c12a02c136fcc17f01fe51b6372d33a6a7eda4913d2d1b1f86203
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package com.tangem.core.ui.res.generated
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Auto-generated from design tokens. Do not edit manually.
|
||||
* Theme: Dark
|
||||
*/
|
||||
internal class TangemDarkColorTokens {
|
||||
// color.text
|
||||
val colorTextPrimary = Color(0xFFFFFFFF)
|
||||
val colorTextSecondary = Color(0x99FFFFFF)
|
||||
val colorTextTertiary = Color(0x4DFFFFFF)
|
||||
val colorTextBrand = Color(0xFF0090F9)
|
||||
val colorTextStaticLightPrimary = Color(0xFF000000)
|
||||
val colorTextStaticLightSecondary = Color(0x99000000)
|
||||
val colorTextStaticLightTertiary = Color(0x66000000)
|
||||
val colorTextStaticDarkPrimary = Color(0xFFFFFFFF)
|
||||
val colorTextStaticDarkSecondary = Color(0x99FFFFFF)
|
||||
val colorTextStaticDarkTertiary = Color(0x4DFFFFFF)
|
||||
val colorTextInversePrimary = Color(0xFF0F0F0F)
|
||||
val colorTextInverseSecondary = Color(0x990F0F0F)
|
||||
val colorTextInverseTertiary = Color(0x660F0F0F)
|
||||
val colorTextStatusSuccess = Color(0xFF2DAE3B)
|
||||
val colorTextStatusError = Color(0xFFFF5E66)
|
||||
val colorTextStatusWarning = Color(0xFFEFA210)
|
||||
val colorTextStatusInfo = Color(0xFF109FF0)
|
||||
val colorTextAccentBlue = Color(0xFF109FF0)
|
||||
val colorTextAccentViolet = Color(0xFFB07BFD)
|
||||
val colorTextAccentRed = Color(0xFFFF5E66)
|
||||
val colorTextAccentOrange = Color(0xFFFA6931)
|
||||
val colorTextAccentYellow = Color(0xFFEFA210)
|
||||
val colorTextAccentGreen = Color(0xFF2DAE3B)
|
||||
|
||||
// color.bg
|
||||
val colorBgPrimary = Color(0xFF0F0F0F)
|
||||
val colorBgSecondary = Color(0xFF1B1B1B)
|
||||
val colorBgTertiary = Color(0xFF2C2C2C)
|
||||
val colorBgBrand = Color(0xFF0090F9)
|
||||
val colorBgInverse = Color(0xFFF4F4F4)
|
||||
val colorBgBase = Color(0xFF000000)
|
||||
val colorBgOpaquePrimary = Color(0x0DFFFFFF)
|
||||
val colorBgOpaqueSecondary = Color(0x1AFFFFFF)
|
||||
val colorBgStatusSuccess = Color(0xFF2DA30D)
|
||||
val colorBgStatusError = Color(0xFFF25508)
|
||||
val colorBgStatusWarning = Color(0xFFE68A03)
|
||||
val colorBgStatusInfo = Color(0xFF0090F9)
|
||||
val colorBgAccentBlue = Color(0xFF0090F9)
|
||||
val colorBgAccentViolet = Color(0xFFA967FD)
|
||||
val colorBgAccentRed = Color(0xFFFE4142)
|
||||
val colorBgAccentOrange = Color(0xFFF25508)
|
||||
val colorBgAccentYellow = Color(0xFFE68A03)
|
||||
val colorBgAccentGreen = Color(0xFF2DA30D)
|
||||
|
||||
// color.icon
|
||||
val colorIconPrimary = Color(0xFFFFFFFF)
|
||||
val colorIconSecondary = Color(0x99FFFFFF)
|
||||
val colorIconTertiary = Color(0x4DFFFFFF)
|
||||
val colorIconBrand = Color(0xFF0090F9)
|
||||
val colorIconStaticLight = Color(0xFF000000)
|
||||
val colorIconStaticDark = Color(0xFFFFFFFF)
|
||||
val colorIconInverse = Color(0xFF0F0F0F)
|
||||
val colorIconStatusSuccess = Color(0xFF2DAE3B)
|
||||
val colorIconStatusError = Color(0xFFFA6931)
|
||||
val colorIconStatusWarning = Color(0xFFEFA210)
|
||||
val colorIconStatusInfo = Color(0xFF109FF0)
|
||||
val colorIconAccentBlue = Color(0xFF109FF0)
|
||||
val colorIconAccentViolet = Color(0xFFB07BFD)
|
||||
val colorIconAccentRed = Color(0xFFFF5E66)
|
||||
val colorIconAccentOrange = Color(0xFFFA6931)
|
||||
val colorIconAccentYellow = Color(0xFFEFA210)
|
||||
val colorIconAccentGreen = Color(0xFF2DAE3B)
|
||||
|
||||
// color.border
|
||||
val colorBorderPrimary = Color(0x0DFFFFFF)
|
||||
val colorBorderSecondary = Color(0x1AFFFFFF)
|
||||
val colorBorderTertiary = Color(0x33FFFFFF)
|
||||
val colorBorderBrand = Color(0xFF0090F9)
|
||||
val colorBorderInversePrimary = Color(0x0D000000)
|
||||
val colorBorderInverseSecondary = Color(0x1A000000)
|
||||
val colorBorderInverseTertiary = Color(0x33000000)
|
||||
val colorBorderStatusSuccess = Color(0xFF2DAE3B)
|
||||
val colorBorderStatusError = Color(0xFFFA6931)
|
||||
val colorBorderStatusWarning = Color(0xFFEFA210)
|
||||
val colorBorderStatusInfo = Color(0xFF109FF0)
|
||||
val colorBorderAccentBlue = Color(0xFF109FF0)
|
||||
val colorBorderAccentViolet = Color(0xFFB07BFD)
|
||||
val colorBorderAccentRed = Color(0xFFFF5E66)
|
||||
val colorBorderAccentOrange = Color(0xFFFA6931)
|
||||
val colorBorderAccentYellow = Color(0xFFEFA210)
|
||||
val colorBorderAccentGreen = Color(0xFF2DAE3B)
|
||||
|
||||
// color.overlay
|
||||
val colorOverlayModal = Color(0xCC000000)
|
||||
|
||||
// color.interaction
|
||||
val colorInteractionPress = Color(0x1AFFFFFF)
|
||||
val colorInteractionPressStaticLight = Color(0x1A000000)
|
||||
val colorInteractionPressStaticDark = Color(0x1AFFFFFF)
|
||||
val colorInteractionPressInverse = Color(0x1A000000)
|
||||
|
||||
// color.material
|
||||
val colorMaterialTintGlass = Color(0x66181818)
|
||||
val colorMaterialTintBlur = Color(0x00000000)
|
||||
val colorMaterialTintSolid = Color(0x1AFFFFFF)
|
||||
val colorMaterialFillGlass = Color(0x00000000)
|
||||
val colorMaterialFillBlur = Color(0x1AFFFFFF)
|
||||
val colorMaterialFillSolid = Color(0xE62C2C2C)
|
||||
val colorMaterialLightenGlass = Color(0x33181818)
|
||||
val colorMaterialLightenBlur = Color(0x00000000)
|
||||
val colorMaterialLightenSolid = Color(0x00000000)
|
||||
val colorMaterialSoftLightGlass = Color(0x1A000000)
|
||||
val colorMaterialSoftLightBlur = Color(0x00000000)
|
||||
val colorMaterialSoftLightSolid = Color(0x00000000)
|
||||
val colorMaterialBorderStart = Color(0x33FFFFFF)
|
||||
val colorMaterialBorderMid = Color(0x00FFFFFF)
|
||||
val colorMaterialBorderEnd = Color(0x1AFFFFFF)
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package com.tangem.core.ui.res.generated
|
||||
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Auto-generated from design tokens. Do not edit manually.
|
||||
*/
|
||||
internal class TangemDimensionTokens {
|
||||
// blur
|
||||
val blurSizeCard = 48.dp
|
||||
val blurSizeButton = 32.dp
|
||||
|
||||
// border-radius
|
||||
val borderRadius100 = 8.dp
|
||||
val borderRadius150 = 12.dp
|
||||
val borderRadius200 = 16.dp
|
||||
val borderRadius250 = 20.dp
|
||||
val borderRadius300 = 24.dp
|
||||
val borderRadius400 = 32.dp
|
||||
val borderRadiusNone = 0.dp
|
||||
val borderRadius075 = 6.dp
|
||||
val borderRadius050 = 4.dp
|
||||
val borderRadiusFull = 999.dp
|
||||
|
||||
// border-width
|
||||
val borderWidthNone = 0.dp
|
||||
val borderWidthXs = 0.5.dp
|
||||
val borderWidthSm = 1.dp
|
||||
val borderWidthMd = 2.dp
|
||||
val borderWidthLg = 4.dp
|
||||
|
||||
// size
|
||||
val size100 = 8.dp
|
||||
val size125 = 10.dp
|
||||
val size150 = 12.dp
|
||||
val size200 = 16.dp
|
||||
val size250 = 20.dp
|
||||
val size300 = 24.dp
|
||||
val size350 = 28.dp
|
||||
val size400 = 32.dp
|
||||
val size450 = 36.dp
|
||||
val size500 = 40.dp
|
||||
val size550 = 44.dp
|
||||
val size600 = 48.dp
|
||||
val size700 = 56.dp
|
||||
val size800 = 64.dp
|
||||
val size1000 = 80.dp
|
||||
val size1100 = 88.dp
|
||||
val size1200 = 96.dp
|
||||
val size025 = 2.dp
|
||||
val size050 = 4.dp
|
||||
val sizeCardSm = 128.dp
|
||||
|
||||
// spacing
|
||||
val spacing100 = 8.dp
|
||||
val spacing150 = 12.dp
|
||||
val spacing200 = 16.dp
|
||||
val spacing250 = 20.dp
|
||||
val spacing300 = 24.dp
|
||||
val spacing350 = 28.dp
|
||||
val spacing400 = 32.dp
|
||||
val spacing450 = 36.dp
|
||||
val spacing500 = 40.dp
|
||||
val spacing550 = 44.dp
|
||||
val spacing600 = 48.dp
|
||||
val spacing700 = 56.dp
|
||||
val spacing800 = 64.dp
|
||||
val spacing1000 = 80.dp
|
||||
val spacing025 = 2.dp
|
||||
val spacing050 = 4.dp
|
||||
val spacingNone = 0.dp
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package com.tangem.core.ui.res.generated
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
/**
|
||||
* Auto-generated from design tokens. Do not edit manually.
|
||||
* Theme: Light
|
||||
*/
|
||||
internal class TangemLightColorTokens {
|
||||
// color.text
|
||||
val colorTextPrimary = Color(0xFF0F0F0F)
|
||||
val colorTextSecondary = Color(0x990F0F0F)
|
||||
val colorTextTertiary = Color(0x660F0F0F)
|
||||
val colorTextBrand = Color(0xFF0090F9)
|
||||
val colorTextStaticLightPrimary = Color(0xFF000000)
|
||||
val colorTextStaticLightSecondary = Color(0x99000000)
|
||||
val colorTextStaticLightTertiary = Color(0x66000000)
|
||||
val colorTextStaticDarkPrimary = Color(0xFFFFFFFF)
|
||||
val colorTextStaticDarkSecondary = Color(0x99FFFFFF)
|
||||
val colorTextStaticDarkTertiary = Color(0x4DFFFFFF)
|
||||
val colorTextInversePrimary = Color(0xFFFFFFFF)
|
||||
val colorTextInverseSecondary = Color(0x99FFFFFF)
|
||||
val colorTextInverseTertiary = Color(0x4DFFFFFF)
|
||||
val colorTextStatusSuccess = Color(0xFF2DA30D)
|
||||
val colorTextStatusError = Color(0xFFFE4142)
|
||||
val colorTextStatusWarning = Color(0xFFE68A03)
|
||||
val colorTextStatusInfo = Color(0xFF0090F9)
|
||||
val colorTextAccentBlue = Color(0xFF0090F9)
|
||||
val colorTextAccentViolet = Color(0xFFA967FD)
|
||||
val colorTextAccentRed = Color(0xFFFE4142)
|
||||
val colorTextAccentOrange = Color(0xFFF25508)
|
||||
val colorTextAccentYellow = Color(0xFFE68A03)
|
||||
val colorTextAccentGreen = Color(0xFF2DA30D)
|
||||
|
||||
// color.bg
|
||||
val colorBgPrimary = Color(0xFFF4F4F4)
|
||||
val colorBgSecondary = Color(0xFFFFFFFF)
|
||||
val colorBgTertiary = Color(0xFFEAEAEA)
|
||||
val colorBgBrand = Color(0xFF0090F9)
|
||||
val colorBgInverse = Color(0xFFF4F4F4)
|
||||
val colorBgBase = Color(0xFFF4F4F4)
|
||||
val colorBgOpaquePrimary = Color(0x0DFFFFFF)
|
||||
val colorBgOpaqueSecondary = Color(0x1AFFFFFF)
|
||||
val colorBgStatusSuccess = Color(0xFF2DA30D)
|
||||
val colorBgStatusError = Color(0xFFF25508)
|
||||
val colorBgStatusWarning = Color(0xFFE68A03)
|
||||
val colorBgStatusInfo = Color(0xFF0090F9)
|
||||
val colorBgAccentBlue = Color(0xFF0090F9)
|
||||
val colorBgAccentViolet = Color(0xFFA967FD)
|
||||
val colorBgAccentRed = Color(0xFFFE4142)
|
||||
val colorBgAccentOrange = Color(0xFFF25508)
|
||||
val colorBgAccentYellow = Color(0xFFE68A03)
|
||||
val colorBgAccentGreen = Color(0xFF2DA30D)
|
||||
|
||||
// color.icon
|
||||
val colorIconPrimary = Color(0xFF0F0F0F)
|
||||
val colorIconSecondary = Color(0x990F0F0F)
|
||||
val colorIconTertiary = Color(0x660F0F0F)
|
||||
val colorIconBrand = Color(0xFF0090F9)
|
||||
val colorIconStaticLight = Color(0xFF000000)
|
||||
val colorIconStaticDark = Color(0xFFFFFFFF)
|
||||
val colorIconInverse = Color(0xFFFFFFFF)
|
||||
val colorIconStatusSuccess = Color(0xFF2DA30D)
|
||||
val colorIconStatusError = Color(0xFFF25508)
|
||||
val colorIconStatusWarning = Color(0xFFE68A03)
|
||||
val colorIconStatusInfo = Color(0xFF0090F9)
|
||||
val colorIconAccentBlue = Color(0xFF0090F9)
|
||||
val colorIconAccentViolet = Color(0xFFA967FD)
|
||||
val colorIconAccentRed = Color(0xFFFE4142)
|
||||
val colorIconAccentOrange = Color(0xFFF25508)
|
||||
val colorIconAccentYellow = Color(0xFFE68A03)
|
||||
val colorIconAccentGreen = Color(0xFF2DA30D)
|
||||
|
||||
// color.border
|
||||
val colorBorderPrimary = Color(0x0D000000)
|
||||
val colorBorderSecondary = Color(0x1A000000)
|
||||
val colorBorderTertiary = Color(0x33000000)
|
||||
val colorBorderBrand = Color(0xFF0090F9)
|
||||
val colorBorderInversePrimary = Color(0x0D000000)
|
||||
val colorBorderInverseSecondary = Color(0x1A000000)
|
||||
val colorBorderInverseTertiary = Color(0x33000000)
|
||||
val colorBorderStatusSuccess = Color(0xFF2DA30D)
|
||||
val colorBorderStatusError = Color(0xFFF25508)
|
||||
val colorBorderStatusWarning = Color(0xFFE68A03)
|
||||
val colorBorderStatusInfo = Color(0xFF0090F9)
|
||||
val colorBorderAccentBlue = Color(0xFF0090F9)
|
||||
val colorBorderAccentViolet = Color(0xFFA967FD)
|
||||
val colorBorderAccentRed = Color(0xFFFE4142)
|
||||
val colorBorderAccentOrange = Color(0xFFF25508)
|
||||
val colorBorderAccentYellow = Color(0xFFE68A03)
|
||||
val colorBorderAccentGreen = Color(0xFF2DA30D)
|
||||
|
||||
// color.overlay
|
||||
val colorOverlayModal = Color(0x99000000)
|
||||
|
||||
// color.interaction
|
||||
val colorInteractionPress = Color(0x1A000000)
|
||||
val colorInteractionPressStaticLight = Color(0x1A000000)
|
||||
val colorInteractionPressStaticDark = Color(0x1AFFFFFF)
|
||||
val colorInteractionPressInverse = Color(0x1AFFFFFF)
|
||||
|
||||
// color.material
|
||||
val colorMaterialTintGlass = Color(0x00000000)
|
||||
val colorMaterialTintBlur = Color(0x00000000)
|
||||
val colorMaterialTintSolid = Color(0x1AFFFFFF)
|
||||
val colorMaterialFillGlass = Color(0x00000000)
|
||||
val colorMaterialFillBlur = Color(0x99FFFFFF)
|
||||
val colorMaterialFillSolid = Color(0xE6FFFFFF)
|
||||
val colorMaterialLightenGlass = Color(0x80F7F7F7)
|
||||
val colorMaterialLightenBlur = Color(0x00000000)
|
||||
val colorMaterialLightenSolid = Color(0x00000000)
|
||||
val colorMaterialSoftLightGlass = Color(0x33000000)
|
||||
val colorMaterialSoftLightBlur = Color(0x00000000)
|
||||
val colorMaterialSoftLightSolid = Color(0x00000000)
|
||||
val colorMaterialBorderStart = Color(0x26000000)
|
||||
val colorMaterialBorderMid = Color(0x00000000)
|
||||
val colorMaterialBorderEnd = Color(0x1A000000)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.tangem.core.ui.res.generated
|
||||
|
||||
/**
|
||||
* Auto-generated from design tokens. Do not edit manually.
|
||||
*/
|
||||
internal class TangemOpacityTokens {
|
||||
val opacity0 = 0f
|
||||
val opacity5 = 0.05f
|
||||
val opacity10 = 0.1f
|
||||
val opacity15 = 0.15f
|
||||
val opacity20 = 0.2f
|
||||
val opacity25 = 0.25f
|
||||
val opacity30 = 0.3f
|
||||
val opacity40 = 0.4f
|
||||
val opacity50 = 0.5f
|
||||
val opacity60 = 0.6f
|
||||
val opacity70 = 0.7f
|
||||
val opacity80 = 0.8f
|
||||
val opacity90 = 0.9f
|
||||
val opacity100 = 1f
|
||||
val opacityDisabled = 0.4f
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.tangem.core.ui.res.generated
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
/**
|
||||
* Auto-generated from design tokens. Do not edit manually.
|
||||
*/
|
||||
internal class TangemShadowTokens {
|
||||
val shadowButtonBlur = 40.dp
|
||||
val shadowButtonOffsetX = 0.dp
|
||||
val shadowButtonOffsetY = 8.dp
|
||||
val shadowButtonSpread = 0.dp
|
||||
val shadowButtonColor = Color(0x1A000000)
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package com.tangem.core.ui.res.generated
|
||||
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.LineBreak
|
||||
import androidx.compose.ui.text.style.LineHeightStyle
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.core.ui.res.InterFamily
|
||||
|
||||
/**
|
||||
* Auto-generated from design tokens. Do not edit manually.
|
||||
*/
|
||||
internal class TangemTypographyTokens {
|
||||
val fontDisplayMedium = TextStyle(
|
||||
fontFamily = InterFamily,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 44.sp,
|
||||
lineHeight = 52.sp,
|
||||
letterSpacing = (-0.92).sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
lineBreak = LineBreak.Heading,
|
||||
)
|
||||
|
||||
val fontHeadingMedium = TextStyle(
|
||||
fontFamily = InterFamily,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 28.sp,
|
||||
lineHeight = 33.sp,
|
||||
letterSpacing = (-0.37).sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
lineBreak = LineBreak.Heading,
|
||||
)
|
||||
|
||||
val fontHeadingSmall = TextStyle(
|
||||
fontFamily = InterFamily,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 20.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = (-0.12).sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
lineBreak = LineBreak.Heading,
|
||||
)
|
||||
|
||||
val fontBodyMedium = TextStyle(
|
||||
fontFamily = InterFamily,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.02.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
)
|
||||
|
||||
val fontSubheadingMedium = TextStyle(
|
||||
fontFamily = InterFamily,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 17.sp,
|
||||
letterSpacing = 0.07.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
)
|
||||
|
||||
val fontCaptionMedium = TextStyle(
|
||||
fontFamily = InterFamily,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.18.sp,
|
||||
lineHeightStyle = LineHeightStyle(
|
||||
alignment = LineHeightStyle.Alignment.Center,
|
||||
trim = LineHeightStyle.Trim.None,
|
||||
),
|
||||
)
|
||||
}
|
||||
1
core/ui/token-gen/.gitignore
vendored
Normal file
1
core/ui/token-gen/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
node_modules/
|
||||
532
core/ui/token-gen/build-tokens.mjs
Normal file
532
core/ui/token-gen/build-tokens.mjs
Normal file
|
|
@ -0,0 +1,532 @@
|
|||
import StyleDictionary from 'style-dictionary';
|
||||
import { register, getTransforms } from '@tokens-studio/sd-transforms';
|
||||
import crypto from 'crypto';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// ── Paths ──────────────────────────────────────────────────────────────────────
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const tokensDir = path.join(__dirname, '..', 'ds-tokens', 'tokens');
|
||||
const outputDir = path.join(
|
||||
__dirname, '..', 'src', 'main', 'java', 'com', 'tangem', 'core', 'ui', 'res', 'generated',
|
||||
);
|
||||
|
||||
const PACKAGE = 'com.tangem.core.ui.res.generated';
|
||||
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
// ── Register sd-transforms ─────────────────────────────────────────────────────
|
||||
// Use CSS platform for sd-transforms to get ts/color/css/hexrgba (resolves rgba to hex).
|
||||
// Compose color conversion is done in the format function instead of as a transform,
|
||||
// because the color/composeColor transform interferes with rgba reference resolution.
|
||||
await register(StyleDictionary, { platform: 'css' });
|
||||
|
||||
// ── Token set definitions ──────────────────────────────────────────────────────
|
||||
// Reference-only sets (provide values for other tokens but not in the output)
|
||||
const coreSets = [
|
||||
'core/palette',
|
||||
'core/font',
|
||||
'core/dimension',
|
||||
];
|
||||
|
||||
// Theme-independent semantic sets
|
||||
const sizeSets = [
|
||||
'semantic/size/opacity',
|
||||
'semantic/size/blur',
|
||||
'semantic/size/border',
|
||||
'semantic/size/size',
|
||||
'semantic/size/spacing',
|
||||
];
|
||||
|
||||
const fontSets = [
|
||||
'semantic/font/sizes/android',
|
||||
'semantic/font/styles',
|
||||
];
|
||||
|
||||
const sharedSets = [
|
||||
'semantic/theme/shadows',
|
||||
'semantic/theme/gradient',
|
||||
];
|
||||
|
||||
// Theme-specific sets
|
||||
// Note: material variant files (glass/blur/solid) define colliding paths and are excluded.
|
||||
// The material color tokens come from materials/light and materials/dark instead.
|
||||
const themeBuilds = {
|
||||
Light: {
|
||||
sets: [
|
||||
...coreSets, ...sizeSets, ...fontSets, ...sharedSets,
|
||||
'semantic/theme/light',
|
||||
'semantic/theme/materials/light',
|
||||
],
|
||||
},
|
||||
Dark: {
|
||||
sets: [
|
||||
...coreSets, ...sizeSets, ...fontSets, ...sharedSets,
|
||||
'semantic/theme/dark',
|
||||
'semantic/theme/materials/dark',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
// For theme-independent builds (dimensions, typography, etc.)
|
||||
const sharedBuildSets = [...coreSets, ...sizeSets, ...fontSets, ...sharedSets];
|
||||
|
||||
for (const [theme, { sets }] of Object.entries(themeBuilds)) {
|
||||
console.log(`${theme} theme: ${sets.length} token sets`);
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Convert token path segments to a camelCase property name */
|
||||
function toCamelCase(segments) {
|
||||
return segments
|
||||
.map((seg, i) => {
|
||||
// kebab-case → camelCase
|
||||
const cleaned = seg.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
|
||||
if (i === 0) return cleaned;
|
||||
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
|
||||
})
|
||||
.join('');
|
||||
}
|
||||
|
||||
/** Check if a token is from core/palette, dimension, or gradient source sets (not for output) */
|
||||
function isSourceOnlyToken(token) {
|
||||
const top = token.path[0];
|
||||
return top === 'palette' || top === 'dimension' || top === 'gradient';
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a resolved CSS color string to Compose Color(0xAARRGGBB).
|
||||
* Handles: #RRGGBB, #AARRGGBB, rgba(r, g, b, a), rgb(r, g, b)
|
||||
*/
|
||||
function toComposeColor(value, tokenPath = '') {
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error(`Unrecognized color value for ${tokenPath}: ${JSON.stringify(value)}`);
|
||||
}
|
||||
|
||||
// #RRGGBB
|
||||
const hex6 = value.match(/^#([0-9a-fA-F]{6})$/);
|
||||
if (hex6) return `Color(0xFF${hex6[1].toUpperCase()})`;
|
||||
|
||||
// #AARRGGBB (8-digit hex)
|
||||
const hex8 = value.match(/^#([0-9a-fA-F]{8})$/);
|
||||
if (hex8) return `Color(0x${hex8[1].toUpperCase()})`;
|
||||
|
||||
// rgba(r, g, b, a)
|
||||
const rgba = value.match(/^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)$/);
|
||||
if (rgba) {
|
||||
const r = parseInt(rgba[1]).toString(16).padStart(2, '0').toUpperCase();
|
||||
const g = parseInt(rgba[2]).toString(16).padStart(2, '0').toUpperCase();
|
||||
const b = parseInt(rgba[3]).toString(16).padStart(2, '0').toUpperCase();
|
||||
const a = Math.round(parseFloat(rgba[4]) * 255).toString(16).padStart(2, '0').toUpperCase();
|
||||
return `Color(0x${a}${r}${g}${b})`;
|
||||
}
|
||||
|
||||
// rgb(r, g, b)
|
||||
const rgb = value.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
|
||||
if (rgb) {
|
||||
const r = parseInt(rgb[1]).toString(16).padStart(2, '0').toUpperCase();
|
||||
const g = parseInt(rgb[2]).toString(16).padStart(2, '0').toUpperCase();
|
||||
const b = parseInt(rgb[3]).toString(16).padStart(2, '0').toUpperCase();
|
||||
return `Color(0xFF${r}${g}${b})`;
|
||||
}
|
||||
|
||||
// Transparent
|
||||
if (value === 'transparent' || value === '#00000000') return 'Color(0x00000000)';
|
||||
|
||||
throw new Error(`Unrecognized color format for ${tokenPath}: "${value}"`);
|
||||
}
|
||||
|
||||
/** Group tokens by their first N path segments */
|
||||
function groupByPath(tokens, depth = 1) {
|
||||
const groups = {};
|
||||
for (const token of tokens) {
|
||||
const key = token.path.slice(0, depth).join('.');
|
||||
if (!groups[key]) groups[key] = [];
|
||||
groups[key].push(token);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
// ── Custom formats ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Kotlin format for color tokens.
|
||||
* Generates: class TangemLightColorTokens / TangemDarkColorTokens
|
||||
*/
|
||||
StyleDictionary.registerFormat({
|
||||
name: 'kotlin/compose-colors',
|
||||
format: ({ dictionary, options }) => {
|
||||
const themeName = options.themeName; // "Light" or "Dark"
|
||||
const objectName = `Tangem${themeName}ColorTokens`;
|
||||
|
||||
const colorTokens = dictionary.allTokens.filter(
|
||||
t => t.$type === 'color' && !isSourceOnlyToken(t),
|
||||
);
|
||||
|
||||
// Group by top-level category (color.text, color.bg, color.icon, etc.)
|
||||
const groups = groupByPath(colorTokens, 2);
|
||||
const sections = [];
|
||||
|
||||
for (const [groupKey, tokens] of Object.entries(groups)) {
|
||||
const comment = ` // ${groupKey}`;
|
||||
const props = tokens.map(token => {
|
||||
const propName = toCamelCase(token.path);
|
||||
const value = toComposeColor(token.$value, token.path.join('.'));
|
||||
return ` val ${propName} = ${value}`;
|
||||
});
|
||||
sections.push([comment, ...props].join('\n'));
|
||||
}
|
||||
|
||||
return [
|
||||
`package ${PACKAGE}`,
|
||||
'',
|
||||
'import androidx.compose.ui.graphics.Color',
|
||||
'',
|
||||
'/**',
|
||||
` * Auto-generated from design tokens. Do not edit manually.`,
|
||||
` * Theme: ${themeName}`,
|
||||
' */',
|
||||
`internal class ${objectName} {`,
|
||||
sections.join('\n\n'),
|
||||
'}',
|
||||
'',
|
||||
].join('\n');
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Kotlin format for dimension tokens (spacing, size, border-radius, border-width).
|
||||
*/
|
||||
StyleDictionary.registerFormat({
|
||||
name: 'kotlin/compose-dimensions',
|
||||
format: ({ dictionary }) => {
|
||||
const dimTokens = dictionary.allTokens.filter(t => {
|
||||
// Exclude font tokens — letter-spacing values are already in typography tokens as sp
|
||||
if (t.path[0] === 'font') return false;
|
||||
return (
|
||||
(t.$type === 'dimension' || t.$type === 'borderRadius' || t.$type === 'borderWidth') &&
|
||||
!isSourceOnlyToken(t)
|
||||
);
|
||||
});
|
||||
|
||||
const groups = groupByPath(dimTokens, 1);
|
||||
const sections = [];
|
||||
|
||||
for (const [groupKey, tokens] of Object.entries(groups)) {
|
||||
const comment = ` // ${groupKey}`;
|
||||
const props = tokens.map(token => {
|
||||
const propName = toCamelCase(token.path);
|
||||
// Resolved value is in px (number), convert to dp
|
||||
const raw = parseFloat(token.$value);
|
||||
if (isNaN(raw)) throw new Error(`Non-numeric dimension value for ${token.path.join('.')}: "${token.$value}"`);
|
||||
const dpVal = `${raw}.dp`;
|
||||
return ` val ${propName} = ${dpVal}`;
|
||||
});
|
||||
sections.push([comment, ...props].join('\n'));
|
||||
}
|
||||
|
||||
return [
|
||||
`package ${PACKAGE}`,
|
||||
'',
|
||||
'import androidx.compose.ui.unit.dp',
|
||||
'',
|
||||
'/**',
|
||||
' * Auto-generated from design tokens. Do not edit manually.',
|
||||
' */',
|
||||
'internal class TangemDimensionTokens {',
|
||||
sections.join('\n\n'),
|
||||
'}',
|
||||
'',
|
||||
].join('\n');
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Kotlin format for opacity tokens.
|
||||
*/
|
||||
StyleDictionary.registerFormat({
|
||||
name: 'kotlin/compose-opacity',
|
||||
format: ({ dictionary }) => {
|
||||
const opacityTokens = dictionary.allTokens.filter(
|
||||
t => t.$type === 'opacity' && !isSourceOnlyToken(t),
|
||||
);
|
||||
|
||||
const props = opacityTokens.map(token => {
|
||||
const propName = toCamelCase(token.path);
|
||||
const raw = parseFloat(token.$value);
|
||||
if (isNaN(raw)) throw new Error(`Non-numeric opacity value for ${token.path.join('.')}: "${token.$value}"`);
|
||||
const floatVal = `${raw}f`;
|
||||
return ` val ${propName} = ${floatVal}`;
|
||||
});
|
||||
|
||||
return [
|
||||
`package ${PACKAGE}`,
|
||||
'',
|
||||
'/**',
|
||||
' * Auto-generated from design tokens. Do not edit manually.',
|
||||
' */',
|
||||
'internal class TangemOpacityTokens {',
|
||||
...props,
|
||||
'}',
|
||||
'',
|
||||
].join('\n');
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Kotlin format for typography tokens.
|
||||
*/
|
||||
StyleDictionary.registerFormat({
|
||||
name: 'kotlin/compose-typography',
|
||||
format: ({ dictionary }) => {
|
||||
const typoTokens = dictionary.allTokens.filter(
|
||||
t => t.$type === 'typography' && !isSourceOnlyToken(t),
|
||||
);
|
||||
|
||||
const props = typoTokens.map(token => {
|
||||
const propName = toCamelCase(token.path);
|
||||
const v = token.$value;
|
||||
|
||||
// v is an object: { fontFamily, fontWeight, fontSize, lineHeight, letterSpacing, ... }
|
||||
const tp = token.path.join('.');
|
||||
if (!v.fontWeight) throw new Error(`Missing fontWeight for ${tp}`);
|
||||
const fontWeight = mapFontWeight(v.fontWeight);
|
||||
const fontSize = parseFloat(v.fontSize);
|
||||
if (isNaN(fontSize)) throw new Error(`Non-numeric fontSize for ${tp}: "${v.fontSize}"`);
|
||||
const lineHeight = parseFloat(v.lineHeight);
|
||||
if (isNaN(lineHeight)) throw new Error(`Non-numeric lineHeight for ${tp}: "${v.lineHeight}"`);
|
||||
const letterSpacing = parseFloat(v.letterSpacing);
|
||||
if (isNaN(letterSpacing)) throw new Error(`Non-numeric letterSpacing for ${tp}: "${v.letterSpacing}"`);
|
||||
|
||||
// display and heading categories get LineBreak.Heading (matches TangemTypography2)
|
||||
const category = token.path[1]; // display, heading, body, subheading, caption
|
||||
const isHeading = category === 'display' || category === 'heading';
|
||||
|
||||
const lines = [
|
||||
` val ${propName} = TextStyle(`,
|
||||
` fontFamily = InterFamily,`,
|
||||
` fontWeight = ${fontWeight},`,
|
||||
` fontSize = ${fontSize}.sp,`,
|
||||
` lineHeight = ${lineHeight}.sp,`,
|
||||
` letterSpacing = ${letterSpacing < 0 ? `(${letterSpacing})` : letterSpacing}.sp,`,
|
||||
` lineHeightStyle = LineHeightStyle(`,
|
||||
` alignment = LineHeightStyle.Alignment.Center,`,
|
||||
` trim = LineHeightStyle.Trim.None,`,
|
||||
` ),`,
|
||||
];
|
||||
if (isHeading) {
|
||||
lines.push(` lineBreak = LineBreak.Heading,`);
|
||||
}
|
||||
lines.push(` )`);
|
||||
|
||||
return lines.join('\n');
|
||||
});
|
||||
|
||||
return [
|
||||
`package ${PACKAGE}`,
|
||||
'',
|
||||
'import androidx.compose.ui.text.TextStyle',
|
||||
'import androidx.compose.ui.text.font.FontWeight',
|
||||
'import androidx.compose.ui.text.style.LineBreak',
|
||||
'import androidx.compose.ui.text.style.LineHeightStyle',
|
||||
'import androidx.compose.ui.unit.sp',
|
||||
'import com.tangem.core.ui.res.InterFamily',
|
||||
'',
|
||||
'/**',
|
||||
' * Auto-generated from design tokens. Do not edit manually.',
|
||||
' */',
|
||||
'internal class TangemTypographyTokens {',
|
||||
props.join('\n\n'),
|
||||
'}',
|
||||
'',
|
||||
].join('\n');
|
||||
},
|
||||
});
|
||||
|
||||
function mapFontWeight(value) {
|
||||
const num = parseInt(value, 10);
|
||||
if (!isNaN(num)) {
|
||||
if (num <= 400) return 'FontWeight.Normal';
|
||||
if (num <= 500) return 'FontWeight.Medium';
|
||||
if (num <= 600) return 'FontWeight.SemiBold';
|
||||
return 'FontWeight.Bold';
|
||||
}
|
||||
const lower = String(value).toLowerCase();
|
||||
if (lower.includes('semibold') || lower.includes('semi bold')) return 'FontWeight.SemiBold';
|
||||
if (lower.includes('bold')) return 'FontWeight.Bold';
|
||||
if (lower.includes('medium')) return 'FontWeight.Medium';
|
||||
return 'FontWeight.Normal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Kotlin format for shadow tokens.
|
||||
* Shadow tokens are composite (type: shadow) with object $value containing
|
||||
* blur, spread, color, offsetX, offsetY, type.
|
||||
*/
|
||||
StyleDictionary.registerFormat({
|
||||
name: 'kotlin/compose-shadows',
|
||||
format: ({ dictionary }) => {
|
||||
const shadowTokens = dictionary.allTokens.filter(
|
||||
t => (t.$type === 'shadow' || t.$type === 'boxShadow') && !isSourceOnlyToken(t),
|
||||
);
|
||||
|
||||
const entries = shadowTokens.map(token => {
|
||||
const propName = toCamelCase(token.path);
|
||||
const v = token.$value;
|
||||
const sp = token.path.join('.');
|
||||
if (!v) throw new Error(`Missing shadow value for ${sp}`);
|
||||
const blur = parseFloat(v.blur);
|
||||
if (isNaN(blur)) throw new Error(`Non-numeric blur for ${sp}: "${v.blur}"`);
|
||||
const spread = parseFloat(v.spread);
|
||||
if (isNaN(spread)) throw new Error(`Non-numeric spread for ${sp}: "${v.spread}"`);
|
||||
const offsetX = parseFloat(v.offsetX);
|
||||
if (isNaN(offsetX)) throw new Error(`Non-numeric offsetX for ${sp}: "${v.offsetX}"`);
|
||||
const offsetY = parseFloat(v.offsetY);
|
||||
if (isNaN(offsetY)) throw new Error(`Non-numeric offsetY for ${sp}: "${v.offsetY}"`);
|
||||
if (!v.color) throw new Error(`Missing color for ${sp}`);
|
||||
const colorVal = toComposeColor(v.color, sp + '.color');
|
||||
|
||||
return [
|
||||
` val ${propName}Blur = ${blur}.dp`,
|
||||
` val ${propName}OffsetX = ${offsetX}.dp`,
|
||||
` val ${propName}OffsetY = ${offsetY}.dp`,
|
||||
` val ${propName}Spread = ${spread}.dp`,
|
||||
` val ${propName}Color = ${colorVal}`,
|
||||
].join('\n');
|
||||
});
|
||||
|
||||
return [
|
||||
`package ${PACKAGE}`,
|
||||
'',
|
||||
'import androidx.compose.ui.graphics.Color',
|
||||
'import androidx.compose.ui.unit.dp',
|
||||
'',
|
||||
'/**',
|
||||
' * Auto-generated from design tokens. Do not edit manually.',
|
||||
' */',
|
||||
'internal class TangemShadowTokens {',
|
||||
entries.join('\n\n'),
|
||||
'}',
|
||||
'',
|
||||
].join('\n');
|
||||
},
|
||||
});
|
||||
|
||||
// ── Build ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
const composePlatformTransforms = [
|
||||
...getTransforms({ platform: 'css' }),
|
||||
'name/camel',
|
||||
];
|
||||
|
||||
// Build color tokens per theme (light/dark)
|
||||
for (const [themeName, { sets }] of Object.entries(themeBuilds)) {
|
||||
console.log(`\nBuilding ${themeName} color tokens...`);
|
||||
|
||||
const sd = new StyleDictionary({
|
||||
source: sets.map(s => path.join(tokensDir, `${s}.json`)),
|
||||
preprocessors: ['tokens-studio'],
|
||||
usesDtcg: true,
|
||||
log: { warnings: 'disabled', errors: { brokenReferences: 'console' } },
|
||||
platforms: {
|
||||
compose: {
|
||||
transforms: composePlatformTransforms,
|
||||
buildPath: outputDir + '/',
|
||||
files: [
|
||||
{
|
||||
destination: `Tangem${themeName}ColorTokens.kt`,
|
||||
format: 'kotlin/compose-colors',
|
||||
options: { themeName },
|
||||
filter: token => token.$type === 'color' && !isSourceOnlyToken(token),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await sd.buildAllPlatforms();
|
||||
console.log(` ✓ Tangem${themeName}ColorTokens.kt`);
|
||||
}
|
||||
|
||||
// Build theme-independent tokens (dimensions, opacity, typography, shadows)
|
||||
console.log('\nBuilding dimension, opacity, typography, and shadow tokens...');
|
||||
|
||||
const sd = new StyleDictionary({
|
||||
source: sharedBuildSets.map(s => path.join(tokensDir, `${s}.json`)),
|
||||
preprocessors: ['tokens-studio'],
|
||||
usesDtcg: true,
|
||||
log: { warnings: 'disabled', errors: { brokenReferences: 'console' } },
|
||||
platforms: {
|
||||
compose: {
|
||||
transforms: composePlatformTransforms,
|
||||
buildPath: outputDir + '/',
|
||||
files: [
|
||||
{
|
||||
destination: 'TangemDimensionTokens.kt',
|
||||
format: 'kotlin/compose-dimensions',
|
||||
filter: token => {
|
||||
const t = token.$type;
|
||||
return (
|
||||
token.path[0] !== 'font' &&
|
||||
(t === 'dimension' || t === 'borderRadius' || t === 'borderWidth') &&
|
||||
!isSourceOnlyToken(token)
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
destination: 'TangemOpacityTokens.kt',
|
||||
format: 'kotlin/compose-opacity',
|
||||
filter: token => token.$type === 'opacity' && !isSourceOnlyToken(token),
|
||||
},
|
||||
{
|
||||
destination: 'TangemTypographyTokens.kt',
|
||||
format: 'kotlin/compose-typography',
|
||||
filter: token => token.$type === 'typography' && !isSourceOnlyToken(token),
|
||||
},
|
||||
{
|
||||
destination: 'TangemShadowTokens.kt',
|
||||
format: 'kotlin/compose-shadows',
|
||||
filter: token => (token.$type === 'shadow' || token.$type === 'boxShadow') && !isSourceOnlyToken(token),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await sd.buildAllPlatforms();
|
||||
console.log(' ✓ TangemDimensionTokens.kt');
|
||||
console.log(' ✓ TangemOpacityTokens.kt');
|
||||
console.log(' ✓ TangemTypographyTokens.kt');
|
||||
console.log(' ✓ TangemShadowTokens.kt');
|
||||
|
||||
// ── Write source hash ─────────────────────────────────────────────────────────
|
||||
// Hash all token JSON files so Gradle can verify generated code matches ds-tokens.
|
||||
function computeTokensHash() {
|
||||
const files = [];
|
||||
function walk(dir) {
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) walk(full);
|
||||
else if (entry.name.endsWith('.json')) files.push(full);
|
||||
}
|
||||
}
|
||||
walk(tokensDir);
|
||||
files.sort(); // deterministic order
|
||||
|
||||
const hash = crypto.createHash('sha256');
|
||||
for (const file of files) {
|
||||
hash.update(path.relative(tokensDir, file).split(path.sep).join('/'));
|
||||
hash.update('\0');
|
||||
hash.update(fs.readFileSync(file));
|
||||
hash.update('\0');
|
||||
}
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
const tokensHash = computeTokensHash();
|
||||
fs.writeFileSync(path.join(outputDir, '.tokens-hash'), tokensHash + '\n');
|
||||
console.log(` ✓ .tokens-hash (${tokensHash.substring(0, 12)}…)`);
|
||||
|
||||
console.log(`\nDone! Output: ${outputDir}`);
|
||||
1749
core/ui/token-gen/package-lock.json
generated
Normal file
1749
core/ui/token-gen/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
13
core/ui/token-gen/package.json
Normal file
13
core/ui/token-gen/package.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "tangem-token-gen",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "node build-tokens.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"style-dictionary": "^5.4.0",
|
||||
"@tokens-studio/sd-transforms": "^2.0.3"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue