Initial commit

This commit is contained in:
2025-09-22 19:25:32 -03:00
parent dcd261a2b3
commit e0062445b6
122 changed files with 4728 additions and 19 deletions

View File

@@ -0,0 +1,22 @@
group = "dev.carlosmartino.designsystem"
plugins {
alias(libs.plugins.kotlinDevKit)
alias(libs.plugins.composeDevKit)
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation(projects.core.common)
api(libs.insetsx)
implementation(libs.markdown)
}
}
}
compose.resources {
publicResClass = true
packageOfResClass = "dev.carlosmartino.designsystem.resources"
generateResClass = always
}

View File

@@ -0,0 +1,39 @@
package cl.homelogic.platform.designsystem.themes
import cl.homelogic.platform.common.logging.Trace
import cl.homelogic.platform.common.logging.TreeRoots
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
enum class StatusBarState {
LIGHT,
DARK,
}
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual class StatusBarManager : IStatusBarManager {
private val _coroutineScope = CoroutineScope(Dispatchers.Default)
companion object {
private val _statusBarState = MutableStateFlow(StatusBarState.LIGHT)
val state: StateFlow<StatusBarState> = _statusBarState.asStateFlow()
}
override fun lightMode() {
Trace.d(TreeRoots.DesignSystem, "Android - requesting light status bar")
_coroutineScope.launch {
_statusBarState.emit(StatusBarState.LIGHT)
}
}
override fun darkMode() {
Trace.d(TreeRoots.DesignSystem, "Android - requesting dark status bar")
_coroutineScope.launch {
_statusBarState.emit(StatusBarState.DARK)
}
}
}

View File

@@ -0,0 +1,12 @@
package cl.homelogic.platform.designsystem.components.atoms
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import cl.homelogic.platform.designsystem.foundations.Sizes
@Composable
fun BottomSafeZone() {
Spacer(modifier = Modifier.height(Sizes.ScreenInset.Bottom))
}

View File

@@ -0,0 +1,86 @@
package cl.homelogic.platform.designsystem.components.atoms
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import cl.homelogic.platform.designsystem.extensions.conditionalClickable
import cl.homelogic.platform.designsystem.extensions.scaledByFontSize
import cl.homelogic.platform.designsystem.foundations.Padding
import cl.homelogic.platform.designsystem.foundations.Shapes
import cl.homelogic.platform.designsystem.foundations.Sizes
@Composable
fun DescriptionCard(
modifier: Modifier = Modifier,
title: String,
subtitle: String = "",
onClick: (() -> Unit)? = null,
content: @Composable () -> Unit,
) {
val interactionSource = remember { MutableInteractionSource() }
Surface(
modifier = modifier
.padding(top = Padding.tiny, bottom = Padding.small)
.clip(Shapes.RoundCorner.regular)
.wrapContentSize(Alignment.TopCenter, false)
.conditionalClickable(
interactionSource = interactionSource,
indication = LocalIndication.current,
onClick = onClick,
),
shape = Shapes.RoundCorner.regular,
border = BorderStroke(
width = Sizes.Borders.Regular,
color = MaterialTheme.colorScheme.outline,
),
) {
Column(
modifier = Modifier.wrapContentSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
content()
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.surfaceVariant)
.padding(Padding.regular)
.height(Sizes.Containers.Small.scaledByFontSize())
.fillMaxWidth(),
) {
Text(
text = title,
textAlign = TextAlign.Start,
style = typography.labelLarge,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = subtitle,
textAlign = TextAlign.Start,
style = typography.labelMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}

View File

@@ -0,0 +1,20 @@
package cl.homelogic.platform.designsystem.components.atoms
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import cl.homelogic.platform.designsystem.foundations.Sizes
@Composable
fun Divider(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.fillMaxWidth()
.height(Sizes.Stroke.Regular)
.background(MaterialTheme.colorScheme.outline),
)
}

View File

@@ -0,0 +1,856 @@
package cl.homelogic.platform.designsystem.components.atoms
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import cl.homelogic.platform.designsystem.foundations.Padding
import cl.homelogic.platform.designsystem.foundations.Shapes
import org.intellij.markdown.MarkdownElementTypes
import org.intellij.markdown.MarkdownTokenTypes
import org.intellij.markdown.ast.ASTNode
import org.intellij.markdown.ast.getTextInNode
import org.intellij.markdown.flavours.gfm.GFMElementTypes
import org.intellij.markdown.flavours.gfm.GFMFlavourDescriptor
import org.intellij.markdown.parser.MarkdownParser
@Immutable
data class MarkdownTypography(
val text: TextStyle,
val code: TextStyle,
val inlineCode: TextStyle,
val h1: TextStyle,
val h2: TextStyle,
val h3: TextStyle,
val h4: TextStyle,
val h5: TextStyle,
val h6: TextStyle,
val quote: TextStyle,
val paragraph: TextStyle,
val ordered: TextStyle,
val bullet: TextStyle,
val list: TextStyle,
val textLink: TextLinkStates,
val table: TextStyle,
)
@Immutable
data class MarkdownColors(
val codeBackground: Color,
val codeText: Color,
val dividerColor: Color,
val inlineCodeBackground: Color,
val inlineCodeText: Color,
val linkText: Color,
val tableBackground: Color,
val tableText: Color,
val text: Color,
)
@Immutable
data class TextLinkStates(
val normal: TextStyle,
val visited: TextStyle,
)
@Immutable
private data class MarkdownContentData(
val node: ASTNode,
val content: String,
val colors: MarkdownColors,
val typography: MarkdownTypography,
val level: Int = 0,
)
@Composable
fun Markdown(
modifier: Modifier = Modifier,
text: String,
colors: MarkdownColors = rememberDefaultMarkdownColors(),
typography: MarkdownTypography = rememberDefaultMarkdownTypography(),
) {
val flavour = remember { GFMFlavourDescriptor() }
val parser = remember { MarkdownParser(flavour) }
val markdownTree = remember(text) { parser.buildMarkdownTreeFromString(text) }
val markdownContent = remember(markdownTree, text, colors, typography) {
MarkdownContentData(markdownTree, text, colors, typography)
}
val annotatedStringCache = remember { mutableStateMapOf<String, AnnotatedString>() }
val listState = rememberLazyListState()
SelectionContainer {
Column {
OptimizedMarkdownContent(
data = markdownContent,
stringCache = annotatedStringCache,
)
}
}
}
@Composable
private fun rememberDefaultMarkdownColors(): MarkdownColors {
val colorScheme = MaterialTheme.colorScheme
return remember {
MarkdownColors(
codeBackground = colorScheme.surfaceVariant,
codeText = colorScheme.onSurfaceVariant,
dividerColor = colorScheme.outline,
inlineCodeBackground = colorScheme.surfaceVariant,
inlineCodeText = colorScheme.onSurfaceVariant,
linkText = colorScheme.tertiary,
tableBackground = colorScheme.surface,
tableText = colorScheme.onSurface,
text = colorScheme.onPrimary,
)
}
}
@Composable
private fun rememberDefaultMarkdownTypography(): MarkdownTypography {
val materialTypography = MaterialTheme.typography
return remember {
MarkdownTypography(
bullet = materialTypography.bodySmall,
code = materialTypography.bodyMedium,
h1 = materialTypography.headlineLarge,
h2 = materialTypography.headlineMedium,
h3 = materialTypography.headlineSmall,
h4 = materialTypography.titleLarge,
h5 = materialTypography.titleMedium,
h6 = materialTypography.bodyLarge,
inlineCode = materialTypography.bodyLarge,
list = materialTypography.bodyMedium,
ordered = materialTypography.bodyMedium,
paragraph = materialTypography.bodyMedium,
quote = materialTypography.bodyMedium.copy(
fontStyle = FontStyle.Italic,
),
table = materialTypography.bodySmall,
text = materialTypography.bodyMedium,
textLink = TextLinkStates(
normal = materialTypography.bodyMedium.copy(
textDecoration = TextDecoration.Underline,
),
visited = materialTypography.bodyMedium.copy(
textDecoration = TextDecoration.Underline,
),
),
)
}
}
@Composable
private fun OptimizedMarkdownContent(
data: MarkdownContentData,
stringCache: MutableMap<String, AnnotatedString>,
maxDepth: Int = 10,
) {
if (data.level > maxDepth) return
for (child in data.node.children) {
val nodeKey = "${child.hashCode()}-${data.level}"
val childData = remember(child, data) {
data.copy(node = child, level = data.level + 1)
}
when (child.type) {
MarkdownElementTypes.PARAGRAPH -> {
OptimizedParagraph(stringCache, nodeKey, child, data)
}
MarkdownElementTypes.ATX_1,
MarkdownElementTypes.ATX_2,
MarkdownElementTypes.ATX_3,
MarkdownElementTypes.ATX_4,
MarkdownElementTypes.ATX_5,
MarkdownElementTypes.ATX_6,
-> {
OptimizedHeading(
childData = childData,
nodeKey = nodeKey,
stringCache = stringCache,
)
}
MarkdownElementTypes.UNORDERED_LIST -> {
OptimizedUnorderedList(
childData = childData,
stringCache = stringCache,
)
}
MarkdownElementTypes.ORDERED_LIST -> {
OptimizedOrderedList(
childData = childData,
stringCache = stringCache,
)
}
MarkdownElementTypes.BLOCK_QUOTE -> {
OptimizedBlockQuote(
childData = childData,
stringCache = stringCache,
)
}
MarkdownElementTypes.CODE_FENCE,
MarkdownElementTypes.CODE_BLOCK,
-> {
OptimizedCodeBlock(childData = childData)
}
MarkdownElementTypes.HTML_BLOCK -> {
OptimizedHtmlBlock(childData = childData)
}
GFMElementTypes.TABLE -> {
OptimizedTable(
childData = childData,
stringCache = stringCache,
)
}
}
}
}
@Composable
private fun OptimizedParagraph(
stringCache: MutableMap<String, AnnotatedString>,
nodeKey: String,
child: ASTNode,
data: MarkdownContentData,
) {
val paragraphModifier = remember {
Modifier.padding(
vertical = Padding.small,
horizontal = Padding.tiny,
)
}
val annotatedString = stringCache.getOrPut(nodeKey) {
buildAnnotatedString {
renderInlineContent(child, data.content, data.colors, data.typography)
}
}
Text(
text = annotatedString,
style = data.typography.paragraph,
color = data.colors.text,
modifier = paragraphModifier,
)
}
@Composable
private fun OptimizedHeading(
childData: MarkdownContentData,
nodeKey: String,
stringCache: MutableMap<String, AnnotatedString>,
) {
val (style, topPadding, bottomPadding) = remember(childData.node.type) {
when (childData.node.type) {
MarkdownElementTypes.ATX_1 -> Triple(
childData.typography.h1,
Padding.regular,
Padding.small,
)
MarkdownElementTypes.ATX_2 -> Triple(
childData.typography.h2,
Padding.small,
Padding.small,
)
MarkdownElementTypes.ATX_3 -> Triple(
childData.typography.h3,
Padding.small,
Padding.small,
)
MarkdownElementTypes.ATX_4 -> Triple(
childData.typography.h4,
Padding.small,
Padding.tiny,
)
MarkdownElementTypes.ATX_5 -> Triple(
childData.typography.h5,
Padding.small,
Padding.tiny,
)
MarkdownElementTypes.ATX_6 -> Triple(
childData.typography.h6,
Padding.small,
Padding.tiny,
)
else -> Triple(
childData.typography.text,
Padding.small,
Padding.small,
)
}
}
val headingModifier = remember(topPadding, bottomPadding) {
Modifier.padding(top = topPadding, bottom = bottomPadding)
}
val headingText = stringCache.getOrPut(nodeKey) {
buildAnnotatedString {
for (headerChild in childData.node.children) {
if (headerChild.type != MarkdownTokenTypes.ATX_HEADER &&
headerChild.type != MarkdownTokenTypes.EOL
) {
renderInlineContent(
headerChild,
childData.content,
childData.colors,
childData.typography,
)
}
}
}
}
Text(
text = headingText,
style = style,
color = childData.colors.text,
modifier = headingModifier,
)
}
@Composable
private fun OptimizedUnorderedList(
childData: MarkdownContentData,
stringCache: MutableMap<String, AnnotatedString>,
) {
val listModifier = remember {
Modifier
.padding(vertical = Padding.small)
.padding(start = Padding.regular)
}
Column(modifier = listModifier) {
for (item in childData.node.children) {
if (item.type == MarkdownElementTypes.LIST_ITEM) {
val itemData = remember(item, childData) {
childData.copy(node = item)
}
OptimizedListItem(
itemData = itemData,
stringCache = stringCache,
isOrdered = false,
index = 0,
)
}
}
}
}
@Composable
private fun OptimizedOrderedList(
childData: MarkdownContentData,
stringCache: MutableMap<String, AnnotatedString>,
) {
val listModifier = remember {
Modifier
.padding(vertical = Padding.small)
.padding(start = Padding.regular)
}
Column(modifier = listModifier) {
childData.node.children.forEachIndexed { index, item ->
if (item.type == MarkdownElementTypes.LIST_ITEM) {
val itemData = remember(item, childData) {
childData.copy(node = item)
}
OptimizedListItem(
itemData = itemData,
stringCache = stringCache,
isOrdered = true,
index = index + 1,
)
}
}
}
}
@Composable
private fun OptimizedListItem(
itemData: MarkdownContentData,
stringCache: MutableMap<String, AnnotatedString>,
isOrdered: Boolean,
index: Int,
) {
val itemModifier = remember { Modifier.padding(vertical = Padding.tiny) }
Row(
modifier = itemModifier,
verticalAlignment = Alignment.Top,
) {
Text(
text = if (isOrdered) "$index. " else "",
style = if (isOrdered) itemData.typography.ordered else itemData.typography.bullet,
color = itemData.colors.text,
)
Column {
for (itemChild in itemData.node.children) {
val nodeKey = "${itemChild.hashCode()}-${itemData.level}"
when (itemChild.type) {
MarkdownElementTypes.PARAGRAPH -> {
val text = stringCache.getOrPut(nodeKey) {
buildAnnotatedString {
renderInlineContent(
itemChild,
itemData.content,
itemData.colors,
itemData.typography,
)
}
}
Text(
text = text,
style = itemData.typography.list,
color = itemData.colors.text,
)
}
MarkdownElementTypes.UNORDERED_LIST,
MarkdownElementTypes.ORDERED_LIST,
-> {
val childData = remember(itemChild, itemData) {
itemData.copy(node = itemChild)
}
OptimizedMarkdownContent(
data = childData,
stringCache = stringCache,
)
}
}
}
}
}
}
@Composable
private fun OptimizedBlockQuote(
childData: MarkdownContentData,
stringCache: MutableMap<String, AnnotatedString>,
) {
val blockQuoteModifier = remember { Modifier.padding(vertical = Padding.small) }
val dividerModifier = remember {
Modifier
.width(Padding.tiny)
.wrapContentHeight()
.background(childData.colors.dividerColor)
}
val contentModifier = remember { Modifier.padding(start = Padding.regular) }
Row(
modifier = blockQuoteModifier,
verticalAlignment = Alignment.Top,
) {
Box(modifier = dividerModifier)
Column(modifier = contentModifier) {
for (quoteChild in childData.node.children) {
val nodeKey = "${quoteChild.hashCode()}-${childData.level}"
if (quoteChild.type == MarkdownElementTypes.PARAGRAPH) {
val text = stringCache.getOrPut(nodeKey) {
buildAnnotatedString {
renderInlineContent(
quoteChild,
childData.content,
childData.colors,
childData.typography,
)
}
}
Text(
text = text,
style = childData.typography.quote,
color = childData.colors.text,
)
} else {
val innerChildData = remember(quoteChild, childData) {
childData.copy(node = quoteChild)
}
OptimizedMarkdownContent(
data = innerChildData,
stringCache = stringCache,
)
}
}
}
}
}
@Composable
private fun OptimizedCodeBlock(childData: MarkdownContentData) {
val codeContent = remember(childData.node, childData.content) {
when (childData.node.type) {
MarkdownElementTypes.CODE_FENCE -> {
childData.node.children
.firstOrNull { it.type == MarkdownTokenTypes.CODE_FENCE_CONTENT }
?.getTextInNode(childData.content)
?.toString() ?: ""
}
MarkdownElementTypes.CODE_BLOCK -> {
childData.node.children
.firstOrNull { it.type == MarkdownTokenTypes.CODE_LINE }
?.getTextInNode(childData.content)
?.toString() ?: ""
}
else -> ""
}
}
val codeBlockModifier = remember {
Modifier
.clip(Shapes.RoundCorner.regular)
.background(childData.colors.codeBackground)
.fillMaxWidth()
.padding(vertical = Padding.small)
}
val textModifier = remember { Modifier.padding(Padding.regular) }
Surface(
modifier = codeBlockModifier,
color = childData.colors.codeBackground,
) {
Text(
text = codeContent,
style = childData.typography.code,
color = childData.colors.codeText,
modifier = textModifier,
)
}
}
@Composable
private fun OptimizedHtmlBlock(childData: MarkdownContentData) {
val htmlContent = remember(childData.node, childData.content) {
childData.node.getTextInNode(childData.content).toString()
}
val htmlBlockModifier = remember { Modifier.padding(vertical = Padding.small) }
Text(
text = htmlContent,
style = childData.typography.text,
color = childData.colors.text,
modifier = htmlBlockModifier,
)
}
@Composable
private fun OptimizedTable(
childData: MarkdownContentData,
stringCache: MutableMap<String, AnnotatedString>,
) {
val tableModifier = remember {
Modifier
.fillMaxWidth()
.padding(vertical = Padding.small)
}
val tableContentModifier = remember { Modifier.padding(Padding.small) }
Surface(
color = childData.colors.tableBackground,
modifier = tableModifier,
) {
Column(modifier = tableContentModifier) {
for (row in childData.node.children) {
if (row.type == GFMElementTypes.HEADER ||
row.type == GFMElementTypes.ROW
) {
val rowData = remember(row, childData) {
childData.copy(node = row)
}
OptimizedTableRow(
rowData = rowData,
stringCache = stringCache,
)
if (row.type == GFMElementTypes.HEADER) {
Divider(modifier = Modifier.padding(bottom = Padding.small))
}
}
}
}
}
}
@Composable
private fun OptimizedTableRow(
rowData: MarkdownContentData,
stringCache: MutableMap<String, AnnotatedString>,
) {
val rowModifier = remember { Modifier.fillMaxWidth() }
val cellNodes = remember(rowData.node) {
rowData.node.children.filter {
it.type != GFMElementTypes.HEADER &&
it.type != GFMElementTypes.ROW &&
it.type != GFMElementTypes.TABLE
}
}
Row(
modifier = rowModifier,
horizontalArrangement = Arrangement.SpaceBetween,
) {
cellNodes.forEach { cellNode ->
val cellData = remember(cellNode, rowData) {
rowData.copy(node = cellNode)
}
val isHeader = remember(rowData.node.type) {
rowData.node.type == GFMElementTypes.HEADER
}
OptimizedTableCell(
modifier = Modifier.weight(1f),
cellData = cellData,
stringCache = stringCache,
isHeader = isHeader,
)
}
}
}
@Composable
private fun OptimizedTableCell(
modifier: Modifier = Modifier,
cellData: MarkdownContentData,
stringCache: MutableMap<String, AnnotatedString>,
isHeader: Boolean,
) {
val nodeKey = "${cellData.node.hashCode()}-${cellData.level}"
val cellModifier = remember { modifier.padding(Padding.tiny) }
val cellText = stringCache.getOrPut(nodeKey) {
buildAnnotatedString {
renderInlineContent(
cellData.node,
cellData.content,
cellData.colors,
cellData.typography,
)
}
}
val style = remember(isHeader) {
if (isHeader) cellData.typography.h4 else cellData.typography.table
}
val fontWeight = remember(isHeader) {
if (isHeader) FontWeight.Bold else FontWeight.Normal
}
Text(
text = cellText,
style = style,
color = cellData.colors.tableText,
fontWeight = fontWeight,
modifier = cellModifier,
)
}
private fun AnnotatedString.Builder.renderInlineContent(
node: ASTNode,
content: String,
colors: MarkdownColors,
typography: MarkdownTypography,
) {
for (child in node.children) {
when (child.type) {
MarkdownTokenTypes.TEXT, MarkdownTokenTypes.WHITE_SPACE -> {
append(child.getTextInNode(content))
}
MarkdownElementTypes.EMPH -> {
renderEmphasis(child, content, colors, typography)
}
MarkdownElementTypes.STRONG -> {
renderStrong(child, content, colors, typography)
}
MarkdownElementTypes.CODE_SPAN -> {
renderCodeSpan(child, content, colors)
}
MarkdownElementTypes.INLINE_LINK -> {
renderInlineLink(child, content, colors, typography)
}
MarkdownElementTypes.FULL_REFERENCE_LINK,
MarkdownElementTypes.SHORT_REFERENCE_LINK,
-> {
renderReferenceLink(child, content, colors, typography)
}
MarkdownTokenTypes.HARD_LINE_BREAK -> {
append("\n")
}
else -> {
renderInlineContent(child, content, colors, typography)
}
}
}
}
private fun AnnotatedString.Builder.renderEmphasis(
node: ASTNode,
content: String,
colors: MarkdownColors,
typography: MarkdownTypography,
) {
pushStyle(SpanStyle(fontStyle = FontStyle.Italic))
renderInlineContent(node, content, colors, typography)
pop()
}
private fun AnnotatedString.Builder.renderStrong(
node: ASTNode,
content: String,
colors: MarkdownColors,
typography: MarkdownTypography,
) {
pushStyle(SpanStyle(fontWeight = FontWeight.Bold))
renderInlineContent(node, content, colors, typography)
pop()
}
private fun AnnotatedString.Builder.renderCodeSpan(
node: ASTNode,
content: String,
colors: MarkdownColors,
) {
pushStyle(
SpanStyle(
background = colors.inlineCodeBackground,
color = colors.inlineCodeText,
fontFamily = FontFamily.Monospace,
),
)
append("${node.getTextInNode(content).trim('`')}")
pop()
}
private fun AnnotatedString.Builder.renderInlineLink(
node: ASTNode,
content: String,
colors: MarkdownColors,
typography: MarkdownTypography,
) {
val linkText = node.children.firstOrNull { it.type == MarkdownElementTypes.LINK_TEXT }
val linkDestination =
node.children.firstOrNull { it.type == MarkdownElementTypes.LINK_DESTINATION }
val url = linkDestination?.getTextInNode(content)?.trim('(', ')')?.toString() ?: ""
val startPosition = length
pushStyle(SpanStyle(color = colors.linkText))
if (linkText != null) {
val linkTextContent = linkText.getTextInNode(content)
if (linkTextContent.isNotEmpty()) {
append(linkTextContent)
} else {
for (textChild in linkText.children) {
if (textChild.type != MarkdownTokenTypes.LBRACKET &&
textChild.type != MarkdownTokenTypes.RBRACKET
) {
renderInlineContent(textChild, content, colors, typography)
}
}
}
} else {
append(url)
}
if (length > startPosition) {
addLink(
url = LinkAnnotation.Url(url = url),
start = startPosition,
end = length,
)
}
pop()
}
private fun AnnotatedString.Builder.renderReferenceLink(
node: ASTNode,
content: String,
colors: MarkdownColors,
typography: MarkdownTypography,
) {
val linkText = node.children.firstOrNull { it.type == MarkdownElementTypes.LINK_TEXT }
pushStyle(
SpanStyle(
color = colors.linkText,
textDecoration = TextDecoration.Underline,
),
)
if (linkText != null) {
for (textChild in linkText.children) {
if (textChild.type != MarkdownTokenTypes.LBRACKET &&
textChild.type != MarkdownTokenTypes.RBRACKET
) {
renderInlineContent(textChild, content, colors, typography)
}
}
}
pop()
}

View File

@@ -0,0 +1,36 @@
package cl.homelogic.platform.designsystem.components.atoms
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import cl.homelogic.platform.designsystem.foundations.Padding
import cl.homelogic.platform.designsystem.foundations.Sizes
@Composable
fun SectionTitle(
text: String,
modifier: Modifier = Modifier,
) {
Box(
contentAlignment = Alignment.BottomStart,
modifier = modifier
.fillMaxWidth()
.height(Sizes.Containers.Small)
.padding(bottom = Padding.small),
) {
Box {
Text(
text = text,
style = typography.headlineMedium,
overflow = TextOverflow.Visible,
)
}
}
}

View File

@@ -0,0 +1,145 @@
package cl.homelogic.platform.designsystem.components.atoms
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntOffset
import cl.homelogic.platform.designsystem.foundations.FeedbackType
import cl.homelogic.platform.designsystem.foundations.LocalHapticManager
import cl.homelogic.platform.designsystem.foundations.Padding
import cl.homelogic.platform.designsystem.foundations.Shapes
import cl.homelogic.platform.designsystem.foundations.Sizes
import kotlin.math.abs
import kotlin.math.roundToInt
@Composable
fun SegmentedControl(
modifier: Modifier = Modifier,
items: List<String>,
initialSelectedIndex: Int,
onSelectedIndexChange: (Int) -> Unit,
) {
var selectedIndex by remember { mutableIntStateOf(initialSelectedIndex) }
val density = LocalDensity.current
val haptic = LocalHapticManager.current
var componentWidth by remember { mutableStateOf(0) }
val itemWidth = remember(componentWidth) { componentWidth.toFloat() / items.size }
val pillOffset by animateFloatAsState(
targetValue = itemWidth * selectedIndex,
animationSpec = spring(
dampingRatio = 0.83f,
stiffness = Spring.StiffnessMediumLow,
),
label = "SegmentedControl-pillOffset",
)
Box(
modifier = modifier
.height(Sizes.Containers.Small)
.clip(Shapes.RoundCorner.regular)
.background(MaterialTheme.colorScheme.surfaceVariant)
.padding(Padding.tiny)
.onSizeChanged { size ->
if (componentWidth != size.width) {
componentWidth = size.width
}
},
) {
// Animated Pill
Box(
modifier = Modifier
.offset { IntOffset(pillOffset.roundToInt(), 0) }
.width(with(density) { itemWidth.toDp() })
.fillMaxHeight()
.shadow(Sizes.Shadows.Regular, shape = Shapes.RoundCorner.small)
.clip(Shapes.RoundCorner.small)
.background(MaterialTheme.colorScheme.primaryContainer),
)
SegmentedControlItems(items, selectedIndex) { newIndex ->
if (newIndex != selectedIndex) {
// Do a stronger vibration if the pill moves more than
val difference = abs(newIndex - selectedIndex)
if (difference < 3) {
haptic.performFeedback(FeedbackType.SegmentTick)
} else {
haptic.performFeedback(FeedbackType.LongPress)
}
}
selectedIndex = newIndex
onSelectedIndexChange(newIndex)
}
}
}
@Composable
private fun SegmentedControlItems(
items: List<String>,
selectedIndex: Int,
onSelectedIndexChange: (Int) -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
items.forEachIndexed { index, item ->
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
) {
onSelectedIndexChange(index)
}.padding(horizontal = Padding.small),
contentAlignment = Alignment.Center,
) {
Text(
text = item,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = if (selectedIndex == index) {
MaterialTheme.colorScheme.onSurface
} else {
MaterialTheme.colorScheme.onSurfaceVariant
},
style = if (selectedIndex == index) {
typography.bodyLarge
} else {
typography.bodyMedium
},
)
}
}
}
}

View File

@@ -0,0 +1,83 @@
package cl.homelogic.platform.designsystem.components.atoms
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import cl.homelogic.platform.designsystem.extensions.clickable
import cl.homelogic.platform.designsystem.extensions.scaledByFontSize
import cl.homelogic.platform.designsystem.foundations.FeedbackType
import cl.homelogic.platform.designsystem.foundations.LocalHapticManager
import cl.homelogic.platform.designsystem.foundations.Padding
import cl.homelogic.platform.designsystem.foundations.Shapes
import cl.homelogic.platform.designsystem.foundations.Sizes
import cl.homelogic.platform.designsystem.foundations.Symbols
enum class ButtonStyle {
PRIMARY,
SECONDARY,
TERTIARY,
}
@Composable
fun SimpleButton(
modifier: Modifier = Modifier,
style: ButtonStyle = ButtonStyle.PRIMARY,
text: String = "",
leadingSymbol: Symbols? = null,
enabled: Boolean = true,
hapticFeedback: Boolean = true,
onClick: () -> Unit,
) {
val haptic = LocalHapticManager.current
val (background, textColor) = when (style) {
ButtonStyle.PRIMARY -> {
MaterialTheme.colorScheme.tertiary to MaterialTheme.colorScheme.onTertiary
}
ButtonStyle.SECONDARY -> {
MaterialTheme.colorScheme.tertiaryContainer to MaterialTheme.colorScheme.onSurface
}
ButtonStyle.TERTIARY -> {
Color.Transparent to MaterialTheme.colorScheme.onSurface
}
}
Button(
onClick = {
if (hapticFeedback) haptic.performFeedback(FeedbackType.SegmentTick)
onClick()
},
shape = Shapes.RoundCorner.big,
enabled = enabled,
colors = ButtonDefaults.buttonColors().copy(
containerColor = background,
contentColor = textColor,
disabledContainerColor = background,
disabledContentColor = textColor,
),
modifier = modifier
.fillMaxWidth()
.height(Sizes.Buttons.Regular.scaledByFontSize())
.clickable(enabled),
) {
SymbolText(
modifier = Modifier.padding(end = Padding.small),
color = textColor,
icon = leadingSymbol,
)
Text(
text = text,
style = typography.bodyLarge,
)
}
}

View File

@@ -0,0 +1,45 @@
package cl.homelogic.platform.designsystem.components.atoms
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import cl.homelogic.platform.designsystem.extensions.conditionalClickable
import cl.homelogic.platform.designsystem.foundations.Shapes
import cl.homelogic.platform.designsystem.foundations.Sizes
@Composable
fun SimpleCard(
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
content: @Composable () -> Unit,
) {
val interactionSource = remember { MutableInteractionSource() }
Surface(
modifier = modifier
.wrapContentSize(Alignment.TopCenter, false)
.clip(Shapes.RoundCorner.regular)
.fillMaxWidth()
.conditionalClickable(
interactionSource = interactionSource,
indication = LocalIndication.current,
onClick = onClick,
),
shape = Shapes.RoundCorner.regular,
border = BorderStroke(
width = Sizes.Borders.Regular,
color = MaterialTheme.colorScheme.outline,
),
) {
content()
}
}

View File

@@ -0,0 +1,42 @@
package cl.homelogic.platform.designsystem.components.atoms
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CheckboxDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import cl.homelogic.platform.designsystem.extensions.clickable
import cl.homelogic.platform.designsystem.foundations.FeedbackType
import cl.homelogic.platform.designsystem.foundations.LocalHapticManager
@Composable
fun SimpleCheckbox(
checked: Boolean = false,
onCheckedChange: ((Boolean) -> Unit)? = {},
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
val haptic = LocalHapticManager.current
Checkbox(
checked = checked,
onCheckedChange = {
if (it) {
haptic.performFeedback(FeedbackType.ToggleOn)
} else {
haptic.performFeedback(FeedbackType.ToggleOff)
}
onCheckedChange?.invoke(it)
},
modifier = modifier.clickable(enabled),
enabled = enabled,
colors = CheckboxDefaults.colors().copy(
checkedCheckmarkColor = MaterialTheme.colorScheme.onTertiary,
checkedBoxColor = MaterialTheme.colorScheme.tertiary,
checkedBorderColor = MaterialTheme.colorScheme.tertiary,
uncheckedBorderColor = MaterialTheme.colorScheme.outline,
disabledBorderColor = MaterialTheme.colorScheme.tertiary,
disabledCheckedBoxColor = MaterialTheme.colorScheme.tertiary,
),
)
}

View File

@@ -0,0 +1,54 @@
package cl.homelogic.platform.designsystem.components.atoms
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import cl.homelogic.platform.designsystem.foundations.Dimens
import cl.homelogic.platform.designsystem.foundations.Padding
@Composable
fun SimpleWindow(
modifier: Modifier = Modifier.fillMaxSize(),
unbounded: Boolean = false,
topBar: @Composable () -> Unit = {},
content: @Composable ColumnScope.() -> Unit,
) {
val imeVisible by rememberUpdatedState(
WindowInsets.ime.getBottom(LocalDensity.current) > 0,
)
Scaffold(
topBar = topBar,
contentWindowInsets = if (!imeVisible) {
WindowInsets(
left = Dimens.at0,
top = Dimens.at0,
right = Dimens.at0,
bottom = Dimens.at0,
)
} else {
WindowInsets.ime
},
modifier = modifier,
) { padding ->
Surface(modifier = Modifier.padding(padding)) {
Column(
modifier = Modifier.padding(horizontal = Padding.big).takeIf { !unbounded }
?: Modifier,
) {
content()
BottomSafeZone()
}
}
}
}

View File

@@ -0,0 +1,26 @@
package cl.homelogic.platform.designsystem.components.atoms
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import cl.homelogic.platform.designsystem.foundations.Symbols
@Composable
fun SymbolText(
icon: Symbols? = null,
rawIcon: String = "",
style: TextStyle = typography.displaySmall,
color: Color = MaterialTheme.colorScheme.primary,
modifier: Modifier = Modifier,
) {
Text(
modifier = modifier,
text = icon?.codePoint ?: rawIcon,
color = color,
style = style,
)
}

View File

@@ -0,0 +1,78 @@
package cl.homelogic.platform.designsystem.components.molecules
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import cl.homelogic.platform.designsystem.components.atoms.SimpleCard
import cl.homelogic.platform.designsystem.components.atoms.SymbolText
import cl.homelogic.platform.designsystem.extensions.scaledByFontSize
import cl.homelogic.platform.designsystem.foundations.Dimens
import cl.homelogic.platform.designsystem.foundations.FeedbackType
import cl.homelogic.platform.designsystem.foundations.LocalHapticManager
import cl.homelogic.platform.designsystem.foundations.Padding
import cl.homelogic.platform.designsystem.foundations.Sizes
import cl.homelogic.platform.designsystem.foundations.Symbols
@Composable
fun IconShortcut(
modifier: Modifier = Modifier,
icon: Symbols? = null,
rawIcon: String = "",
title: String,
hasNotification: Boolean = false,
onClick: (() -> Unit)? = null,
) {
val haptic = LocalHapticManager.current
SimpleCard(
onClick = {
haptic.performFeedback(FeedbackType.SegmentTick)
onClick?.invoke()
},
modifier = modifier,
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(Sizes.Buttons.Big.scaledByFontSize()),
) {
Row(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surfaceContainer)
.padding(start = Padding.small),
verticalAlignment = Alignment.CenterVertically,
) {
icon?.let { SymbolText(icon = icon) } ?: SymbolText(rawIcon = rawIcon)
Spacer(modifier = Modifier.width(Sizes.Spacers.Regular))
Text(
text = title,
style = typography.labelLarge,
)
}
if (hasNotification) {
Box(
modifier = Modifier
.fillMaxHeight()
.width(Dimens.at8)
.align(Alignment.TopEnd)
.background(MaterialTheme.colorScheme.tertiary),
)
}
}
}
}

View File

@@ -0,0 +1,62 @@
package cl.homelogic.platform.designsystem.components.molecules
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import cl.homelogic.platform.designsystem.components.atoms.BottomSafeZone
import cl.homelogic.platform.designsystem.foundations.Padding
import cl.homelogic.platform.designsystem.foundations.Sizes
data class SectionedListItem(
val title: String,
val composables: List<@Composable () -> Unit>,
)
@Composable
fun LazySectionedList(
modifier: Modifier = Modifier,
items: List<SectionedListItem>,
addSeparator: Boolean,
) {
LazyColumn(
modifier = modifier,
) {
items(items) { sizeGroup ->
Section(sizeGroup.title) {
Column(modifier = Modifier.fillMaxWidth()) {
sizeGroup.composables.forEachIndexed { index, item ->
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(horizontal = Padding.small),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
item()
}
if (index < sizeGroup.composables.size - 1 && addSeparator) {
HorizontalDivider(
thickness = Sizes.Stroke.Regular,
color = MaterialTheme.colorScheme.outline,
)
}
}
}
}
}
item {
BottomSafeZone()
}
}
}

View File

@@ -0,0 +1,207 @@
package cl.homelogic.platform.designsystem.components.molecules
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import cl.homelogic.platform.designsystem.components.atoms.BottomSafeZone
import cl.homelogic.platform.designsystem.components.atoms.ButtonStyle
import cl.homelogic.platform.designsystem.components.atoms.SimpleButton
import cl.homelogic.platform.designsystem.foundations.Padding
import cl.homelogic.platform.designsystem.foundations.Sizes
import kotlinx.coroutines.launch
class OnboardingNavigationContext(
val currentPage: Int,
val pageCount: Int,
private val navigateTo: suspend (Int) -> Unit,
private val restrictNavigation: (Boolean) -> Unit,
) {
val isFirstPage: Boolean get() = currentPage == 0
val isLastPage: Boolean get() = currentPage == pageCount - 1
suspend fun goToNextPage() {
if (currentPage < pageCount - 1) {
navigateTo(currentPage + 1)
}
}
suspend fun goToPreviousPage() {
if (currentPage > 0) {
navigateTo(currentPage - 1)
}
}
fun enableNavigation() {
restrictNavigation(false)
}
}
data class OnboardingSlide(
val restrictNavigation: Boolean = false,
val content: @Composable OnboardingNavigationContext.() -> Unit,
)
@Composable
fun OnboardingWindow(
modifier: Modifier = Modifier,
slides: List<OnboardingSlide>,
continueButtonText: String,
showIndicators: Boolean = true,
onFinish: () -> Unit = {},
) {
if (slides.isEmpty()) {
onFinish()
}
val coroutineScope = rememberCoroutineScope()
val pagerState = rememberPagerState(pageCount = { slides.size })
val isLastPage = pagerState.currentPage == slides.size - 1
val currentSlide = slides[pagerState.currentPage]
var hasRestrictedNavigation by remember(pagerState.currentPage) {
mutableStateOf(currentSlide.restrictNavigation)
}
val navigationContext = remember(pagerState.currentPage, hasRestrictedNavigation) {
OnboardingNavigationContext(
currentPage = pagerState.currentPage,
pageCount = slides.size,
navigateTo = { page -> pagerState.animateScrollToPage(page) },
restrictNavigation = { hasRestrictedNavigation = it },
)
}
Box(
modifier = modifier
.fillMaxSize()
.pointerInput(pagerState.currentPage, hasRestrictedNavigation) {
if (!hasRestrictedNavigation) {
detectHorizontalDragGestures(
onDragStart = {},
onDragEnd = {},
onDragCancel = {},
onHorizontalDrag = { change, dragAmount ->
change.consume()
if (dragAmount > 50 && !navigationContext.isFirstPage) {
coroutineScope.launch {
navigationContext.goToPreviousPage()
}
} else if (dragAmount < -50 && !navigationContext.isLastPage) {
coroutineScope.launch {
navigationContext.goToNextPage()
}
}
},
)
}
},
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.weight(1f)
.wrapContentHeight()
.fillMaxWidth(),
) {
HorizontalPager(
state = pagerState,
modifier = Modifier.fillMaxSize(),
userScrollEnabled = !hasRestrictedNavigation,
) { page ->
val slideNavigationContext = remember(page, pagerState.currentPage) {
OnboardingNavigationContext(
currentPage = page,
pageCount = slides.size,
navigateTo = { targetPage -> pagerState.animateScrollToPage(targetPage) },
restrictNavigation = {
if (page == pagerState.currentPage) {
hasRestrictedNavigation = it
}
},
)
}
slides[page].content(slideNavigationContext)
}
}
if (showIndicators) {
PageIndicators(
pageCount = slides.size,
currentPage = pagerState.currentPage,
modifier = Modifier.padding(Padding.big),
)
}
SimpleButton(
modifier = Modifier
.fillMaxWidth()
.padding(Padding.big),
style = ButtonStyle.TERTIARY,
text = continueButtonText,
enabled = !hasRestrictedNavigation,
) {
coroutineScope.launch {
if (isLastPage) {
onFinish()
} else {
navigationContext.goToNextPage()
}
}
}
BottomSafeZone()
}
}
}
@Composable
private fun PageIndicators(
pageCount: Int,
currentPage: Int,
modifier: Modifier = Modifier,
) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = modifier,
) {
repeat(pageCount) { index ->
val isSelected = index == currentPage
Box(
modifier = Modifier
.padding(horizontal = Padding.tiny)
.size(if (isSelected) Sizes.Dots.Regular else Sizes.Dots.Regular)
.background(
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline,
shape = CircleShape,
),
)
}
}
}

View File

@@ -0,0 +1,87 @@
package cl.homelogic.platform.designsystem.components.molecules
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import cl.homelogic.platform.designsystem.components.atoms.SimpleCard
import cl.homelogic.platform.designsystem.foundations.FeedbackType
import cl.homelogic.platform.designsystem.foundations.LocalHapticManager
import cl.homelogic.platform.designsystem.foundations.Padding
import cl.homelogic.platform.designsystem.foundations.Sizes
data class RadioButtonItem<T>(
val text: String,
val data: T,
)
@Composable
fun <T> RadioButtonSelector(
items: List<RadioButtonItem<T>>,
initialSelection: RadioButtonItem<T>? = null,
onItemSelected: (T) -> Unit,
modifier: Modifier = Modifier,
) {
var selectedItem by remember { mutableStateOf(initialSelection) }
val haptic = LocalHapticManager.current
Column(
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(Sizes.Spacers.Regular),
) {
items.forEach { item ->
SelectorOption(
item = item,
isSelected = item == selectedItem,
onSelect = {
haptic.performFeedback(FeedbackType.SegmentTick)
selectedItem = item
onItemSelected(item.data)
},
)
}
}
}
@Composable
private fun <T> SelectorOption(
item: RadioButtonItem<T>,
isSelected: Boolean,
onSelect: () -> Unit,
) {
SimpleCard(
onClick = onSelect,
modifier = Modifier.wrapContentHeight(),
) {
Row(
modifier = Modifier
.padding(Padding.small)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.padding(start = Padding.small),
text = item.text,
style = MaterialTheme.typography.bodyMedium,
)
RadioButton(
selected = isSelected,
onClick = onSelect,
)
}
}
}

View File

@@ -0,0 +1,113 @@
package cl.homelogic.platform.designsystem.components.molecules
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import cl.homelogic.platform.designsystem.components.atoms.SymbolText
import cl.homelogic.platform.designsystem.extensions.intangible
import cl.homelogic.platform.designsystem.foundations.Padding
import cl.homelogic.platform.designsystem.foundations.Shapes
import cl.homelogic.platform.designsystem.foundations.Sizes
import cl.homelogic.platform.designsystem.foundations.Symbols
@Composable
fun SearchBar(
modifier: Modifier = Modifier,
hint: String = "",
onTextChange: (String) -> Unit = {},
onSearch: (String) -> Unit = {},
) {
var searchText by remember { mutableStateOf("") }
var isFocused by remember { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
Box(
modifier = modifier
.fillMaxWidth()
.defaultMinSize(minHeight = Sizes.TextFields.Regular)
.clip(Shapes.RoundCorner.regular)
.background(MaterialTheme.colorScheme.surfaceVariant)
.clickable {
focusRequester.requestFocus()
}.padding(horizontal = Padding.regular),
contentAlignment = Alignment.CenterStart,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
SymbolText(
icon = Symbols.IconSearch,
style = MaterialTheme.typography.displayMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(modifier = Modifier.width(Sizes.Spacers.Regular))
BasicTextField(
value = searchText,
onValueChange = { newText ->
searchText = newText
onTextChange(newText)
},
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = Padding.regular)
.focusRequester(focusRequester)
.onFocusChanged { focusState ->
isFocused = focusState.isFocused
},
textStyle = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface,
),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Search,
),
keyboardActions = KeyboardActions(
onSearch = {
onSearch(searchText)
focusManager.clearFocus()
},
),
decorationBox = { innerTextField ->
if (searchText.isEmpty() && !isFocused) {
Text(
text = hint,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.intangible(),
)
} else {
innerTextField()
}
},
)
}
}
}

View File

@@ -0,0 +1,15 @@
package cl.homelogic.platform.designsystem.components.molecules
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import cl.homelogic.platform.designsystem.components.atoms.SectionTitle
@Composable
fun Section(
title: String,
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
SectionTitle(title, modifier)
content()
}

View File

@@ -0,0 +1,51 @@
package cl.homelogic.platform.designsystem.components.molecules
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import cl.homelogic.platform.designsystem.components.atoms.SymbolText
import cl.homelogic.platform.designsystem.foundations.FeedbackType
import cl.homelogic.platform.designsystem.foundations.LocalHapticManager
import cl.homelogic.platform.designsystem.foundations.Shapes
import cl.homelogic.platform.designsystem.foundations.Sizes
import cl.homelogic.platform.designsystem.foundations.Symbols
@Composable
fun SymbolButton(
modifier: Modifier = Modifier.size(Sizes.Buttons.Regular),
icon: Symbols,
color: Color = MaterialTheme.colorScheme.primary,
background: Color = Color.Transparent,
hapticFeedback: Boolean = true,
onClick: (() -> Unit)? = null,
) {
val haptic = LocalHapticManager.current
val interactionSource = remember { MutableInteractionSource() }
Box(
modifier = modifier
.clip(Shapes.RoundCorner.regular)
.background(background)
.clickable(
interactionSource = interactionSource,
indication = LocalIndication.current,
onClick = {
if (hapticFeedback) haptic.performFeedback(FeedbackType.SegmentTick)
onClick?.invoke()
},
),
contentAlignment = Alignment.Center,
) {
SymbolText(icon = icon, style = typography.displaySmall, color = color)
}
}

View File

@@ -0,0 +1,213 @@
package cl.homelogic.platform.designsystem.components.organisms
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import cl.homelogic.platform.designsystem.components.molecules.SymbolButton
import cl.homelogic.platform.designsystem.extensions.requirePrecondition
import cl.homelogic.platform.designsystem.foundations.Padding
import cl.homelogic.platform.designsystem.foundations.Sizes
import cl.homelogic.platform.designsystem.foundations.Symbols
sealed class HeaderTypes {
data class Home(
val title: String,
val onSettings: () -> Unit,
) : HeaderTypes()
data class SubHome(
val title: String,
) : HeaderTypes()
data class Step(
val title: String,
val onBack: () -> Unit,
) : HeaderTypes()
data class Modal(
val onClosed: () -> Unit,
) : HeaderTypes()
data class Flex(
val title: String,
val leftShortcuts: List<FlexHeaderShortcut>,
val rightShortcuts: List<FlexHeaderShortcut>,
) : HeaderTypes() {
init {
requirePrecondition(leftShortcuts.size <= 3)
requirePrecondition(rightShortcuts.size <= 3)
}
}
}
data class FlexHeaderShortcut(
val icon: Symbols,
val onClick: () -> Unit,
)
@Composable
fun Header(
type: HeaderTypes,
showDivider: Boolean = true,
modifier: Modifier = Modifier,
) {
Surface(
modifier = modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
.height(Sizes.Header.Regular),
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
when (type) {
is HeaderTypes.Home -> buildHomeHeader(type)
is HeaderTypes.SubHome -> buildSubHomeHeader(type)
is HeaderTypes.Step -> buildFlowHeader(type)
is HeaderTypes.Modal -> buildModalHeader(type)
is HeaderTypes.Flex -> buildFlexHeader(type)
}
if (showDivider) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(Sizes.Stroke.Regular)
.background(MaterialTheme.colorScheme.outline)
.align(Alignment.BottomStart),
)
}
}
}
}
@Composable
private fun buildHomeHeader(type: HeaderTypes.Home) {
Text(
text = type.title,
textAlign = TextAlign.Center,
style = typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Clip,
)
Row(
modifier = Modifier
.fillMaxSize()
.padding(Padding.small),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End,
) {
SymbolButton(
icon = Symbols.IconSettings,
onClick = type.onSettings,
)
}
}
@Composable
private fun buildSubHomeHeader(type: HeaderTypes.SubHome) {
Text(
text = type.title,
textAlign = TextAlign.Center,
style = typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Clip,
)
}
@Composable
private fun buildFlowHeader(type: HeaderTypes.Step) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(Padding.small),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
SymbolButton(
icon = Symbols.IconArrowBack,
onClick = type.onBack,
)
}
Text(
text = type.title,
textAlign = TextAlign.Center,
style = typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Clip,
)
}
@Composable
private fun buildModalHeader(type: HeaderTypes.Modal) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(Padding.small),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
SymbolButton(
icon = Symbols.IconClose,
onClick = type.onClosed,
)
}
}
@Composable
private fun buildFlexHeader(type: HeaderTypes.Flex) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(Padding.small),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
type.leftShortcuts.forEach { shortcut ->
SymbolButton(
icon = shortcut.icon,
onClick = shortcut.onClick,
)
}
}
Text(
text = type.title,
textAlign = TextAlign.Center,
style = typography.titleMedium,
maxLines = 1,
overflow = TextOverflow.Clip,
)
Row(
modifier = Modifier
.fillMaxSize()
.padding(Padding.small),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End,
) {
type.rightShortcuts.forEach { shortcut ->
SymbolButton(
icon = shortcut.icon,
onClick = shortcut.onClick,
)
}
}
}

View File

@@ -0,0 +1,58 @@
package cl.homelogic.platform.designsystem.components.zExperiments
import androidx.compose.foundation.Canvas
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.dp
@Composable
fun TemperatureGraph(
data: List<Float>,
modifier: Modifier = Modifier,
lineColor: Color = Color(0xFF6B7280),
) {
Canvas(modifier = modifier) {
if (data.isEmpty()) return@Canvas
val path = Path()
val points = Path()
val xStep = size.width / (data.size - 1)
val yStep = size.height / (data.maxOrNull() ?: 1f)
// Create smooth curve
path.moveTo(0f, size.height - (data.first() * yStep))
points.moveTo(0f, size.height - (data.first() * yStep))
data.forEachIndexed { index, value ->
if (index == 0) return@forEachIndexed
val x = index * xStep
val y = size.height - (value * yStep)
// Create smooth curve
val controlX1 = ((index - 1) * xStep + x) / 2f
val controlX2 = controlX1
val controlY1 = size.height - (data[index - 1] * yStep)
val controlY2 = y
path.cubicTo(controlX1, controlY1, controlX2, controlY2, x, y)
points.cubicTo(controlX1, controlY1, controlX2, controlY2, x, y)
}
// Draw the line
drawPath(
path = path,
color = lineColor,
style = Stroke(
width = 2.dp.toPx(),
cap = StrokeCap.Round,
join = StrokeJoin.Round,
),
)
}
}

View File

@@ -0,0 +1,9 @@
package cl.homelogic.platform.designsystem.components.zExperiments
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name!")
}

View File

@@ -0,0 +1,95 @@
package cl.homelogic.platform.designsystem.components.zExperiments
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import cl.homelogic.platform.designsystem.foundations.Shapes
@Composable
fun TemperatureCard(
modifier: Modifier = Modifier,
temperature: Int = 72,
percentage: Float = 2f,
temperatureData: List<Float> = emptyList(),
) {
Card(
modifier = modifier
.fillMaxWidth()
.padding(16.dp),
shape = Shapes.RoundCorner.regular,
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
) {
Column(
modifier = Modifier
.padding(24.dp)
.fillMaxWidth(),
) {
Text(
text = "Temperature",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = temperature.toString(),
style = MaterialTheme.typography.displayLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(
text = "Now",
style = MaterialTheme.typography.bodyLarge,
color = Color.Gray,
)
Text(
text = "+${percentage.toInt()}%",
style = MaterialTheme.typography.bodyLarge,
color = Color(0xFF22C55E), // Green color
)
}
Spacer(modifier = Modifier.height(32.dp))
TemperatureGraph(
data = temperatureData,
modifier = Modifier
.fillMaxWidth()
.height(120.dp),
)
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text("9AM", color = Color.Gray)
Text("12PM", color = Color.Gray)
Text("3PM", color = Color.Gray)
Text("6PM", color = Color.Gray)
Text("9PM", color = Color.Gray)
}
}
}
}

View File

@@ -0,0 +1,29 @@
package cl.homelogic.platform.designsystem.extensions
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.semantics.testTag
data class AccessibilityInfo(
val testTag: String,
val contentDescription: String,
val role: Role,
val stateDescription: String? = null,
)
fun Modifier.accessibility(
accessibilityInfo: AccessibilityInfo,
mergeDescendants: Boolean = true,
) = this.semantics(mergeDescendants) {
testTag = accessibilityInfo.testTag
contentDescription = accessibilityInfo.contentDescription
role = accessibilityInfo.role
accessibilityInfo.stateDescription?.let {
stateDescription = it
}
}

View File

@@ -0,0 +1,13 @@
package cl.homelogic.platform.designsystem.extensions
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@Suppress("BanInlineOptIn")
@OptIn(ExperimentalContracts::class)
fun requirePrecondition(value: Boolean) {
contract { returns() implies value }
if (!value) {
throw IllegalArgumentException("Shortcuts cannot exceed 3 items")
}
}

View File

@@ -0,0 +1,15 @@
package cl.homelogic.platform.designsystem.extensions
import androidx.compose.ui.graphics.Color
import kotlin.math.roundToInt
fun Color.toHexString(): String {
val red = (this.red * 255).roundToInt()
val green = (this.green * 255).roundToInt()
val blue = (this.blue * 255).roundToInt()
return "#" + red.toHexString() +
green.toHexString() + blue.toHexString()
}
private fun Int.toHexString(): String = this.toString(16).padStart(2, '0').uppercase()

View File

@@ -0,0 +1,31 @@
package cl.homelogic.platform.designsystem.extensions
import androidx.compose.ui.text.font.FontWeight
enum class FontWeightTypes(
val weight: Int,
) {
Thin(100),
ExtraLight(200),
Light(300),
Normal(400),
Medium(500),
SemiBold(600),
Bold(700),
ExtraBold(800),
Black(900),
}
fun FontWeight.toType(): FontWeightTypes =
when (this.weight) {
FontWeightTypes.Thin.weight -> FontWeightTypes.Thin
FontWeightTypes.ExtraLight.weight -> FontWeightTypes.ExtraLight
FontWeightTypes.Light.weight -> FontWeightTypes.Light
FontWeightTypes.Normal.weight -> FontWeightTypes.Normal
FontWeightTypes.Medium.weight -> FontWeightTypes.Medium
FontWeightTypes.SemiBold.weight -> FontWeightTypes.SemiBold
FontWeightTypes.Bold.weight -> FontWeightTypes.Bold
FontWeightTypes.ExtraBold.weight -> FontWeightTypes.ExtraBold
FontWeightTypes.Black.weight -> FontWeightTypes.Black
else -> FontWeightTypes.Normal
}

View File

@@ -0,0 +1,10 @@
package cl.homelogic.platform.designsystem.extensions
fun String.toUnicode(): String? =
try {
val paddedHex = this.padStart(4, '0')
val codePoint = paddedHex.toInt(16)
codePoint.toChar().toString()
} catch (e: Exception) {
null
}

View File

@@ -0,0 +1,5 @@
package cl.homelogic.platform.designsystem.extensions
import androidx.compose.runtime.Composable
fun listOfComposables(vararg composables: @Composable () -> Unit): List<@Composable () -> Unit> = composables.toList()

View File

@@ -0,0 +1,57 @@
package cl.homelogic.platform.designsystem.extensions
import androidx.compose.foundation.Indication
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.semantics.disabled
import androidx.compose.ui.semantics.semantics
fun Modifier.disable(): Modifier =
this
.alpha(0.3f)
.clickable(enabled = false) {}
.focusable(false)
.semantics {
disabled()
}.pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
val event = awaitPointerEvent(PointerEventPass.Initial)
event.changes.forEach { pointerInputChange: PointerInputChange ->
pointerInputChange.consume()
}
}
}
}
fun Modifier.clickable(status: Boolean): Modifier =
if (status) {
this
} else {
this.disable()
}
fun Modifier.intangible(): Modifier =
this
.alpha(0.3f)
fun Modifier.conditionalClickable(
onClick: (() -> Unit)?,
interactionSource: MutableInteractionSource,
indication: Indication?,
): Modifier =
if (onClick != null) {
this.clickable(
interactionSource = interactionSource,
indication = indication,
onClick = onClick,
)
} else {
this
}

View File

@@ -0,0 +1,11 @@
package cl.homelogic.platform.designsystem.extensions
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
@Composable
fun Dp.scaledByFontSize(): Dp {
val fontScale = LocalDensity.current.fontScale
return this * fontScale
}

View File

@@ -0,0 +1,77 @@
package cl.homelogic.platform.designsystem.foundations
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
enum class FeedbackType {
Confirm,
ContextClick,
GestureEnd,
GestureThresholdActivate,
LongPress,
Reject,
SegmentFrequentTick,
SegmentTick,
TextHandleMove,
ToggleOff,
ToggleOn,
VirtualKey,
}
class HapticManager(
private val hapticFeedback: HapticFeedback,
) {
private val _hapticEnabled = MutableStateFlow(true)
val status: StateFlow<Boolean> = _hapticEnabled
fun enable() {
_hapticEnabled.value = true
}
fun disable() {
_hapticEnabled.value = false
}
fun performFeedback(
type: FeedbackType,
) {
if (!_hapticEnabled.value) return
with(hapticFeedback) {
when (type) {
FeedbackType.Confirm -> performHapticFeedback(HapticFeedbackType.Confirm)
FeedbackType.ContextClick -> performHapticFeedback(HapticFeedbackType.ContextClick)
FeedbackType.GestureEnd -> performHapticFeedback(HapticFeedbackType.GestureEnd)
FeedbackType.GestureThresholdActivate -> performHapticFeedback(HapticFeedbackType.GestureThresholdActivate)
FeedbackType.LongPress -> performHapticFeedback(HapticFeedbackType.LongPress)
FeedbackType.Reject -> performHapticFeedback(HapticFeedbackType.Reject)
FeedbackType.SegmentFrequentTick -> performHapticFeedback(HapticFeedbackType.SegmentFrequentTick)
FeedbackType.SegmentTick -> performHapticFeedback(HapticFeedbackType.SegmentTick)
FeedbackType.TextHandleMove -> performHapticFeedback(HapticFeedbackType.TextHandleMove)
FeedbackType.ToggleOff -> performHapticFeedback(HapticFeedbackType.ToggleOff)
FeedbackType.ToggleOn -> performHapticFeedback(HapticFeedbackType.ToggleOn)
FeedbackType.VirtualKey -> performHapticFeedback(HapticFeedbackType.VirtualKey)
}
}
}
}
val LocalHapticManager = staticCompositionLocalOf<HapticManager> {
error("HapticManager not provided")
}
@Composable
fun HapticSystem(
hapticFeedback: HapticFeedback,
content: @Composable () -> Unit,
) {
val hapticManager = HapticManager(hapticFeedback)
CompositionLocalProvider(LocalHapticManager provides hapticManager) {
content()
}
}

View File

@@ -0,0 +1,201 @@
package cl.homelogic.platform.designsystem.foundations
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
@Immutable
sealed interface Palette {
val name: String
val color: Color
sealed class Mono : Palette {
data object At0 : Mono() {
override val name: String = this.toString()
override val color: Color = Color(0xFFFFFFFF)
}
data object At200 : Mono() {
override val name: String = this.toString()
override val color: Color = Color(0xFFF2F3F5)
}
data object At250 : Mono() {
override val name: String = this.toString()
override val color: Color = Color(0xFFDBE0E5)
}
data object At300 : Mono() {
override val name: String = this.toString()
override val color: Color = Color(0xFFC2C3C5)
}
data object At400 : Mono() {
override val name: String = this.toString()
override val color: Color = Color(0xFF9A9CA0)
}
data object At500 : Mono() {
override val name: String = this.toString()
override val color: Color = Color(0xFF78828D)
}
data object At600 : Mono() {
override val name: String = this.toString()
override val color: Color = Color(0xFF637587)
}
data object At750 : Mono() {
override val name: String = this.toString()
override val color: Color = Color(0xFF48525D)
}
data object At800 : Mono() {
override val name: String = this.toString()
override val color: Color = Color(0xFF3D4754)
}
data object At850 : Mono() {
override val name: String = this.toString()
override val color: Color = Color(0xFF38383D)
}
data object At900 : Mono() {
override val name: String = this.toString()
override val color: Color = Color(0xFF293038)
}
data object At950 : Mono() {
override val name: String = this.toString()
override val color: Color = Color(0xFF121417)
}
}
sealed class Brand : Palette {
data object Phoebe : Brand() {
override val name: String = this.toString()
override val color: Color = Color(0xFF10d48e)
}
data object Joey : Brand() {
override val name: String = this.toString()
override val color: Color = Color(0xFF0fc081)
}
data object Monica : Brand() {
override val name: String = this.toString()
override val color: Color = Color(0xFF132a3a)
}
data object Chandler : Brand() {
override val name: String = this.toString()
override val color: Color = Color(0xFF1c3c52)
}
}
sealed class Sky : Palette {
data object Clear : Sky() {
override val name: String = this.toString()
override val color: Color = Color(0xFF197fe6)
}
data object Deep : Sky() {
override val name: String = this.toString()
override val color: Color = Color(0xFF005CAC)
}
}
sealed class Hazard : Palette {
data object Fire : Hazard() {
override val name: String = this.toString()
override val color: Color = Color(0xFFC34836)
}
}
sealed class Sunset : Palette {
data object Dawn : Sunset() {
override val name: String = this.toString()
override val color: Color = Color(0xFFF49E4E)
}
data object Sunny : Sunset() {
override val name: String = this.toString()
override val color: Color = Color(0xFFFFA255)
}
data object Dusk : Sunset() {
override val name: String = this.toString()
override val color: Color = Color(0xFFF5824E)
}
data object Evening : Sunset() {
override val name: String = this.toString()
override val color: Color = Color(0xFFE07648)
}
data object Twilight : Sunset() {
override val name: String = this.toString()
override val color: Color = Color(0xFF4E3151)
}
data object Night : Sunset() {
override val name: String = this.toString()
override val color: Color = Color(0xFF191538)
}
}
companion object {
fun getMonoColors(): List<Mono> =
listOf(
Mono.At0,
Mono.At200,
Mono.At250,
Mono.At300,
Mono.At400,
Mono.At500,
Mono.At600,
Mono.At750,
Mono.At800,
Mono.At850,
Mono.At900,
Mono.At950,
)
fun getBrandColors(): List<Brand> =
listOf(
Brand.Phoebe,
Brand.Joey,
Brand.Monica,
Brand.Chandler,
)
fun getSkyColors(): List<Sky> =
listOf(
Sky.Clear,
Sky.Deep,
)
fun getHazardColors(): List<Hazard> =
listOf(
Hazard.Fire,
)
fun getSunsetColors(): List<Sunset> =
listOf(
Sunset.Dawn,
Sunset.Sunny,
Sunset.Dusk,
Sunset.Evening,
Sunset.Twilight,
Sunset.Night,
)
fun getAllColors(): List<Palette> =
listOf(
getMonoColors(),
getBrandColors(),
getSkyColors(),
getHazardColors(),
getSunsetColors(),
).flatten()
}
}

View File

@@ -0,0 +1,11 @@
package cl.homelogic.platform.designsystem.foundations
import androidx.compose.foundation.shape.RoundedCornerShape
object Shapes {
object RoundCorner {
val small = RoundedCornerShape(Dimens.at6)
val regular = RoundedCornerShape(Dimens.at8)
val big = RoundedCornerShape(Dimens.at12)
}
}

View File

@@ -0,0 +1,130 @@
package cl.homelogic.platform.designsystem.foundations
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Immutable
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
sealed class Sizes {
object Buttons {
val Small = Dimens.at40
val Regular = Dimens.at48
val Big = Dimens.at56
val Large = Dimens.at72
}
object Dots {
val Small = Dimens.at6
val Regular = Dimens.at8
val Big = Dimens.at10
}
object TextFields {
val Regular = Dimens.at52
}
object Images {
val Regular = Dimens.at300
val Big = Dimens.at350
}
object Borders {
val Regular = Dimens.at1
}
object Header {
val Regular = Dimens.at72
}
object Spacers {
val Regular = Dimens.at8
val Big = Dimens.at16
}
object ScreenInset {
val Bottom = Dimens.at40
}
object Shadows {
val Regular = Dimens.at4
}
object Stroke {
val Regular = Dimens.at1
}
object Containers {
val Small = Dimens.at40
val Regular = Dimens.at60
val Big = Dimens.at72
val Large = Dimens.at120
val Huge = Dimens.at300
}
}
@Immutable
object Padding {
val none = Dimens.at0
val tiny = Dimens.at4
val small = Dimens.at8
val regular = Dimens.at12
val big = Dimens.at16
val large = Dimens.at32
object Content {
val regular = PaddingValues(vertical = Dimens.at16)
}
}
@Immutable
object Dimens {
val at0 = 0.dp
val at1 = 1.dp
val at2 = 2.dp
val at4 = 4.dp
val at6 = 6.dp
val at8 = 8.dp
val at10 = 10.dp
val at12 = 12.dp
val at16 = 16.dp
val at20 = 20.dp
val at24 = 24.dp
val at28 = 28.dp
val at32 = 32.dp
val at36 = 36.dp
val at40 = 40.dp
val at44 = 44.dp
val at48 = 48.dp
val at52 = 52.dp
val at56 = 56.dp
val at60 = 60.dp
val at72 = 72.dp
val at120 = 120.dp
val at300 = 300.dp
val at350 = 350.dp
}
@Immutable
object ScaledDimens {
val at0 = 0.sp
val at2 = 2.sp
val at4 = 4.sp
val at8 = 8.sp
val at12 = 12.sp
val at16 = 16.sp
val at18 = 18.sp
val at20 = 20.sp
val at22 = 22.sp
val at24 = 24.sp
val at26 = 26.sp
val at28 = 28.sp
val at30 = 30.sp
val at32 = 32.sp
val at36 = 36.sp
val at40 = 40.sp
val at44 = 44.sp
val at48 = 48.sp
val at52 = 52.sp
val at56 = 56.sp
val at72 = 72.sp
}

View File

@@ -0,0 +1,60 @@
package cl.homelogic.platform.designsystem.foundations
import cl.homelogic.platform.designsystem.foundations.fonts.materialsymbols.getPlatformIcon
fun Symbols.unicode(): String =
this.codePoint[0]
.code
.toString(16)
.padStart(4, '0')
// Get codepoints from: https://fonts.google.com/icons
sealed class Symbols(
val codePoint: String,
) {
data object IconSettings : Symbols("\ue8b8")
data object IconArrowBack : Symbols(getPlatformIcon("\ue5c4", "\ue5e0"))
data object IconClose : Symbols("\ue5cd")
data object IconTextField : Symbols("\ue9f1")
data object IconButtons : Symbols("\ue72f")
data object IconHeader : Symbols("\uf384")
data object IconSwitchCard : Symbols("\ue1f4")
data object IconFonts : Symbols("\ue262")
data object IconShortcut : Symbols("\ue7e1")
data object IconSymbols : Symbols("\uf7f7")
data object IconHeadline : Symbols("\ue23c")
data object IconHeadlines : Symbols("\ue91a")
data object IconTypography : Symbols("\ueb94")
data object IconTwoColumns : Symbols("\uf847")
data object IconGlyph : Symbols("\ue574")
data object IconColors : Symbols("\ue40a")
data object IconThemes : Symbols("\ue997")
data object IconShapes : Symbols("\ue602")
data object IconDimensions : Symbols("\ue41c")
data object IconIllustrations : Symbols("\ue3f4")
data object IconDarkMode : Symbols("\ue51c")
data object IconSearch : Symbols("\ue8b6")
data object IconPlayCircle : Symbols("\ue1c4")
}

View File

@@ -0,0 +1,51 @@
package cl.homelogic.platform.designsystem.foundations
import cl.homelogic.platform.designsystem.themes.BaseTheme
import cl.homelogic.platform.designsystem.themes.ColorPair
enum class Themes(
val definition: BaseTheme,
) {
ClassicVibes(Classic),
MintVibes(Mint),
}
private object Classic : BaseTheme() {
override val primary = ColorPair(Palette.Mono.At950, Palette.Mono.At0)
override val primaryContainer = ColorPair(Palette.Mono.At0, Palette.Mono.At950)
override val onPrimary = ColorPair(Palette.Mono.At950, Palette.Mono.At0)
override val onPrimaryContainer = ColorPair(Palette.Mono.At0, Palette.Mono.At950)
override val secondary = ColorPair(Palette.Mono.At950, Palette.Mono.At0)
override val secondaryContainer = ColorPair(Palette.Mono.At250, Palette.Mono.At900)
override val tertiary = ColorPair(Palette.Sky.Clear, Palette.Sky.Deep)
override val tertiaryContainer = ColorPair(Palette.Mono.At250, Palette.Mono.At900)
override val onTertiary = ColorPair(Palette.Mono.At0, Palette.Mono.At0)
override val surfaceContainer = ColorPair(Palette.Mono.At0, Palette.Mono.At950)
override val error = ColorPair(Palette.Hazard.Fire, Palette.Hazard.Fire)
override val surface = ColorPair(Palette.Mono.At0, Palette.Mono.At950)
override val surfaceVariant = ColorPair(Palette.Mono.At200, Palette.Mono.At900)
override val onSurface = ColorPair(Palette.Mono.At950, Palette.Mono.At0)
override val onSurfaceVariant = ColorPair(Palette.Mono.At600, Palette.Mono.At0)
override val background = ColorPair(Palette.Mono.At0, Palette.Mono.At950)
override val outline = ColorPair(Palette.Mono.At250, Palette.Mono.At850)
}
private object Mint : BaseTheme() {
override val primary = ColorPair(Palette.Brand.Monica, Palette.Brand.Joey)
override val primaryContainer = ColorPair(Palette.Mono.At0, Palette.Brand.Joey)
override val onPrimary = ColorPair(Palette.Mono.At950, Palette.Mono.At0)
override val onPrimaryContainer = ColorPair(Palette.Mono.At0, Palette.Mono.At950)
override val secondary = ColorPair(Palette.Mono.At950, Palette.Mono.At0)
override val secondaryContainer = ColorPair(Palette.Mono.At250, Palette.Mono.At900)
override val tertiary = ColorPair(Palette.Brand.Phoebe, Palette.Brand.Joey)
override val tertiaryContainer = ColorPair(Palette.Mono.At250, Palette.Mono.At900)
override val onTertiary = ColorPair(Palette.Mono.At0, Palette.Mono.At0)
override val surfaceContainer = ColorPair(Palette.Mono.At0, Palette.Mono.At950)
override val error = ColorPair(Palette.Hazard.Fire, Palette.Hazard.Fire)
override val surface = ColorPair(Palette.Mono.At0, Palette.Mono.At950)
override val surfaceVariant = ColorPair(Palette.Mono.At200, Palette.Mono.At850)
override val onSurface = ColorPair(Palette.Mono.At950, Palette.Mono.At0)
override val onSurfaceVariant = ColorPair(Palette.Mono.At600, Palette.Mono.At0)
override val background = ColorPair(Palette.Mono.At0, Palette.Mono.At950)
override val outline = ColorPair(Palette.Mono.At250, Palette.Mono.At850)
}

View File

@@ -0,0 +1,77 @@
package cl.homelogic.platform.designsystem.foundations
import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.font.FontWeight
import cl.homelogic.platform.designsystem.foundations.fonts.manrope.ManropeFontFamily
import cl.homelogic.platform.designsystem.foundations.fonts.materialsymbols.SymbolsOutlinedFontFamily
@Composable
fun ManropeTypography() =
Typography().run {
val fontFamily = ManropeFontFamily()
copy(
displayLarge = displayLarge.copy(
fontFamily = SymbolsOutlinedFontFamily(),
),
displayMedium = displayMedium.copy(
fontSize = ScaledDimens.at32,
fontFamily = SymbolsOutlinedFontFamily(),
fontWeight = FontWeight.Light,
),
displaySmall = displaySmall.copy(
fontSize = ScaledDimens.at24,
fontFamily = SymbolsOutlinedFontFamily(),
fontWeight = FontWeight.Light,
lineHeight = ScaledDimens.at30,
),
headlineLarge = headlineLarge.copy(
fontFamily = fontFamily,
fontSize = ScaledDimens.at26,
fontWeight = FontWeight.ExtraBold,
lineHeight = ScaledDimens.at48,
),
headlineMedium = headlineMedium.copy(
fontFamily = fontFamily,
fontSize = ScaledDimens.at22,
fontWeight = FontWeight.Bold,
lineHeight = ScaledDimens.at28,
),
headlineSmall = headlineSmall.copy(
fontFamily = fontFamily,
),
titleLarge = titleLarge.copy(
fontFamily = fontFamily,
),
titleMedium = titleMedium.copy(
fontFamily = fontFamily,
fontSize = ScaledDimens.at18,
fontWeight = FontWeight.ExtraBold,
lineHeight = ScaledDimens.at22,
),
titleSmall = titleSmall.copy(
fontFamily = fontFamily,
),
bodyLarge = bodyMedium.copy(
fontFamily = fontFamily,
fontWeight = FontWeight.ExtraBold,
),
bodyMedium = bodyMedium.copy(
fontFamily = fontFamily,
fontWeight = FontWeight.Medium,
),
bodySmall = bodySmall.copy(
fontFamily = fontFamily,
),
labelLarge = labelLarge.copy(
fontFamily = fontFamily,
fontWeight = FontWeight.ExtraBold,
),
labelMedium = labelMedium.copy(
fontFamily = fontFamily,
),
labelSmall = labelSmall.copy(
fontFamily = fontFamily,
),
)
}

View File

@@ -0,0 +1,26 @@
package cl.homelogic.platform.designsystem.foundations.fonts.manrope
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import cl.homelogic.platform.designsystem.resources.Manrope_Bold
import cl.homelogic.platform.designsystem.resources.Manrope_ExtraBold
import cl.homelogic.platform.designsystem.resources.Manrope_ExtraLight
import cl.homelogic.platform.designsystem.resources.Manrope_Light
import cl.homelogic.platform.designsystem.resources.Manrope_Medium
import cl.homelogic.platform.designsystem.resources.Manrope_Regular
import cl.homelogic.platform.designsystem.resources.Manrope_SemiBold
import cl.homelogic.platform.designsystem.resources.Res
import org.jetbrains.compose.resources.Font
@Composable
fun ManropeFontFamily() =
FontFamily(
Font(Res.font.Manrope_ExtraLight, weight = FontWeight.ExtraLight),
Font(Res.font.Manrope_Light, weight = FontWeight.Light),
Font(Res.font.Manrope_Regular, weight = FontWeight.Normal),
Font(Res.font.Manrope_Medium, weight = FontWeight.Medium),
Font(Res.font.Manrope_SemiBold, weight = FontWeight.SemiBold),
Font(Res.font.Manrope_Bold, weight = FontWeight.Bold),
Font(Res.font.Manrope_ExtraBold, weight = FontWeight.ExtraBold),
)

View File

@@ -0,0 +1,119 @@
package cl.homelogic.platform.designsystem.foundations.fonts.materialsymbols
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import cl.homelogic.platform.common.Platform
import cl.homelogic.platform.common.PlatformTypes
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Bold
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_ExtraLight
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Filled_Bold
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Filled_ExtraLight
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Filled_Light
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Filled_Medium
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Filled_Regular
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Filled_SemiBold
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Light
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Medium
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_Regular
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsOutlined_SemiBold
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Bold
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_ExtraLight
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Filled_Bold
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Filled_ExtraLight
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Filled_Light
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Filled_Medium
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Filled_Regular
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Filled_SemiBold
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Light
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Medium
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_Regular
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsRounded_SemiBold
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Bold
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_ExtraLight
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Filled_Bold
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Filled_ExtraLight
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Filled_Light
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Filled_Medium
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Filled_Regular
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Filled_SemiBold
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Light
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Medium
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_Regular
import cl.homelogic.platform.designsystem.resources.MaterialSymbolsSharp_SemiBold
import cl.homelogic.platform.designsystem.resources.Res
import org.jetbrains.compose.resources.Font
@Composable
internal fun SymbolsSharpFontFamily() =
FontFamily(
Font(Res.font.MaterialSymbolsSharp_ExtraLight, weight = FontWeight.ExtraLight),
Font(Res.font.MaterialSymbolsSharp_Light, weight = FontWeight.Light),
Font(Res.font.MaterialSymbolsSharp_Regular, weight = FontWeight.Normal),
Font(Res.font.MaterialSymbolsSharp_Medium, weight = FontWeight.Medium),
Font(Res.font.MaterialSymbolsSharp_SemiBold, weight = FontWeight.SemiBold),
Font(Res.font.MaterialSymbolsSharp_Bold, weight = FontWeight.Bold),
)
@Composable
internal fun SymbolsSharpFilledFontFamily() =
FontFamily(
Font(Res.font.MaterialSymbolsSharp_Filled_ExtraLight, weight = FontWeight.ExtraLight),
Font(Res.font.MaterialSymbolsSharp_Filled_Light, weight = FontWeight.Light),
Font(Res.font.MaterialSymbolsSharp_Filled_Regular, weight = FontWeight.Normal),
Font(Res.font.MaterialSymbolsSharp_Filled_Medium, weight = FontWeight.Medium),
Font(Res.font.MaterialSymbolsSharp_Filled_SemiBold, weight = FontWeight.SemiBold),
Font(Res.font.MaterialSymbolsSharp_Filled_Bold, weight = FontWeight.Bold),
)
@Composable
internal fun SymbolsRoundedFontFamily() =
FontFamily(
Font(Res.font.MaterialSymbolsRounded_ExtraLight, weight = FontWeight.ExtraLight),
Font(Res.font.MaterialSymbolsRounded_Light, weight = FontWeight.Light),
Font(Res.font.MaterialSymbolsRounded_Regular, weight = FontWeight.Normal),
Font(Res.font.MaterialSymbolsRounded_Medium, weight = FontWeight.Medium),
Font(Res.font.MaterialSymbolsRounded_SemiBold, weight = FontWeight.SemiBold),
Font(Res.font.MaterialSymbolsRounded_Bold, weight = FontWeight.Bold),
)
@Composable
internal fun SymbolsRoundedFilledFontFamily() =
FontFamily(
Font(Res.font.MaterialSymbolsRounded_Filled_ExtraLight, weight = FontWeight.ExtraLight),
Font(Res.font.MaterialSymbolsRounded_Filled_Light, weight = FontWeight.Light),
Font(Res.font.MaterialSymbolsRounded_Filled_Regular, weight = FontWeight.Normal),
Font(Res.font.MaterialSymbolsRounded_Filled_Medium, weight = FontWeight.Medium),
Font(Res.font.MaterialSymbolsRounded_Filled_SemiBold, weight = FontWeight.SemiBold),
Font(Res.font.MaterialSymbolsRounded_Filled_Bold, weight = FontWeight.Bold),
)
@Composable
internal fun SymbolsOutlinedFontFamily() =
FontFamily(
Font(Res.font.MaterialSymbolsOutlined_ExtraLight, weight = FontWeight.ExtraLight),
Font(Res.font.MaterialSymbolsOutlined_Light, weight = FontWeight.Light),
Font(Res.font.MaterialSymbolsOutlined_Regular, weight = FontWeight.Normal),
Font(Res.font.MaterialSymbolsOutlined_Medium, weight = FontWeight.Medium),
Font(Res.font.MaterialSymbolsOutlined_SemiBold, weight = FontWeight.SemiBold),
Font(Res.font.MaterialSymbolsOutlined_Bold, weight = FontWeight.Bold),
)
@Composable
internal fun SymbolsOutlinedFilledFontFamily() =
FontFamily(
Font(Res.font.MaterialSymbolsOutlined_Filled_ExtraLight, weight = FontWeight.ExtraLight),
Font(Res.font.MaterialSymbolsOutlined_Filled_Light, weight = FontWeight.Light),
Font(Res.font.MaterialSymbolsOutlined_Filled_Regular, weight = FontWeight.Normal),
Font(Res.font.MaterialSymbolsOutlined_Filled_Medium, weight = FontWeight.Medium),
Font(Res.font.MaterialSymbolsOutlined_Filled_SemiBold, weight = FontWeight.SemiBold),
Font(Res.font.MaterialSymbolsOutlined_Filled_Bold, weight = FontWeight.Bold),
)
internal fun getPlatformIcon(
android: String,
ios: String,
) = when (Platform.getType()) {
PlatformTypes.IOS -> ios
PlatformTypes.Android -> android
}

View File

@@ -0,0 +1,73 @@
package cl.homelogic.platform.designsystem.themes
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import cl.homelogic.platform.designsystem.foundations.Palette
data class ColorPair(
val light: Palette,
val dark: Palette,
)
abstract class BaseTheme {
abstract val primary: ColorPair
abstract val primaryContainer: ColorPair
abstract val onPrimary: ColorPair
abstract val onPrimaryContainer: ColorPair
abstract val secondary: ColorPair
abstract val secondaryContainer: ColorPair
abstract val tertiary: ColorPair
abstract val tertiaryContainer: ColorPair
abstract val onTertiary: ColorPair
abstract val surfaceContainer: ColorPair
abstract val error: ColorPair
abstract val surface: ColorPair
abstract val surfaceVariant: ColorPair
abstract val onSurface: ColorPair
abstract val onSurfaceVariant: ColorPair
abstract val background: ColorPair
abstract val outline: ColorPair
val lightVariant: ColorScheme
get() = lightColorScheme(
primary = primary.light.color,
primaryContainer = primaryContainer.light.color,
onPrimary = onPrimary.light.color,
onPrimaryContainer = onPrimaryContainer.light.color,
secondary = secondary.light.color,
secondaryContainer = secondaryContainer.light.color,
tertiary = tertiary.light.color,
tertiaryContainer = tertiaryContainer.light.color,
onTertiary = onTertiary.light.color,
surfaceContainer = surfaceContainer.light.color,
error = error.light.color,
surface = surface.light.color,
surfaceVariant = surfaceVariant.light.color,
onSurface = onSurface.light.color,
onSurfaceVariant = onSurfaceVariant.light.color,
background = background.light.color,
outline = outline.light.color,
)
val darkVariant: ColorScheme
get() = darkColorScheme(
primary = primary.dark.color,
primaryContainer = primaryContainer.dark.color,
onPrimary = onPrimary.dark.color,
onPrimaryContainer = onPrimaryContainer.dark.color,
secondary = secondary.dark.color,
secondaryContainer = secondaryContainer.dark.color,
tertiary = tertiary.dark.color,
tertiaryContainer = tertiaryContainer.dark.color,
onTertiary = onTertiary.dark.color,
surfaceContainer = surfaceContainer.dark.color,
error = error.dark.color,
surface = surface.dark.color,
surfaceVariant = surfaceVariant.dark.color,
onSurface = onSurface.dark.color,
onSurfaceVariant = onSurfaceVariant.dark.color,
background = background.dark.color,
outline = outline.dark.color,
)
}

View File

@@ -0,0 +1,41 @@
package cl.homelogic.platform.designsystem.themes
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.platform.LocalHapticFeedback
import cl.homelogic.platform.designsystem.foundations.HapticSystem
import cl.homelogic.platform.designsystem.foundations.ManropeTypography
val LocalDesignSystem = staticCompositionLocalOf<DesignSystemManager> {
error("DesignSystemManager not provided")
}
@Composable
fun DesignSystem(
designSystemManager: DesignSystemManager,
content: @Composable () -> Unit,
) {
val state = designSystemManager.state.collectAsState().value
HapticSystem(
hapticFeedback = LocalHapticFeedback.current,
) {
MaterialTheme(
colorScheme = when (state.darkMode) {
DarkModeState.Disabled -> state.theme.definition.lightVariant
DarkModeState.Enabled -> state.theme.definition.darkVariant
DarkModeState.Oled -> state.theme.definition.darkVariant
},
typography = ManropeTypography(),
) {
CompositionLocalProvider(
LocalDesignSystem provides designSystemManager,
) {
content()
}
}
}
}

View File

@@ -0,0 +1,63 @@
package cl.homelogic.platform.designsystem.themes
import cl.homelogic.platform.common.logging.Trace
import cl.homelogic.platform.common.logging.TreeRoots
import cl.homelogic.platform.designsystem.foundations.Themes
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
data class DesignSystemState(
val theme: Themes,
val darkMode: DarkModeState,
)
enum class DarkModeState {
Disabled,
Enabled,
Oled,
}
class DesignSystemManager(
theme: Themes,
darkMode: DarkModeState,
) {
private val _coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private val _statusBarManager = StatusBarManager()
private val _state =
MutableStateFlow(DesignSystemState(theme, darkMode))
val state: StateFlow<DesignSystemState> = _state
init {
Trace.d(TreeRoots.DesignSystem, "Initializing Design System")
}
fun setTheme(theme: Themes) {
Trace.i(TreeRoots.DesignSystem, "Changing theme state to: ${theme.name}")
_coroutineScope.launch {
_state.emit(_state.value.copy(theme = theme))
}
}
fun getTheme(): Themes = _state.value.theme
fun getDarkMode() = _state.value.darkMode
fun setDarkMode(darkMode: DarkModeState) {
Trace.i(TreeRoots.DesignSystem, "Changing dark mode state to: ${darkMode.name}")
when (darkMode) {
DarkModeState.Disabled -> _statusBarManager.lightMode()
DarkModeState.Enabled -> _statusBarManager.darkMode()
DarkModeState.Oled -> _statusBarManager.darkMode()
}
_coroutineScope.launch {
_state.emit(_state.value.copy(darkMode = darkMode))
}
}
}

View File

@@ -0,0 +1,10 @@
package cl.homelogic.platform.designsystem.themes
interface IStatusBarManager {
fun lightMode()
fun darkMode()
}
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
expect class StatusBarManager() : IStatusBarManager

View File

@@ -0,0 +1,27 @@
package cl.homelogic.platform.designsystem.themes
import cl.homelogic.platform.common.logging.Trace
import cl.homelogic.platform.common.logging.TreeRoots
import platform.UIKit.UIApplication
import platform.UIKit.UIStatusBarStyleDarkContent
import platform.UIKit.UIStatusBarStyleLightContent
import platform.UIKit.setStatusBarStyle
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
actual class StatusBarManager : IStatusBarManager {
override fun lightMode() {
Trace.d(TreeRoots.DesignSystem, "iOS - requesting light status bar")
UIApplication.sharedApplication.setStatusBarStyle(
UIStatusBarStyleLightContent,
animated = true,
)
}
override fun darkMode() {
Trace.d(TreeRoots.DesignSystem, "iOS - requesting dark status bar")
UIApplication.sharedApplication.setStatusBarStyle(
UIStatusBarStyleDarkContent,
animated = true,
)
}
}