stevdza-san / KotlinBootstrap

Use the official Bootstrap UI components with Kotlin and Compose HTML, to build a frontend on the web.
203 stars 12 forks source link

add Card Components . #25

Open naser09 opened 2 months ago

naser09 commented 2 months ago

as an android developer i was missing Card component while using this library . so made my own . i was going to do a pull request but as a solo (self taught) developer i am bad at collaboration . so here is the code . please add the component if you think its necessary or usable . or modify it to your liking . maybe its need some more effect like hover effect . but in my case I am using simple card to show my data .

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.stevdza.san.kotlinbs.util.UniqueIdGenerator
import com.varabyte.kobweb.compose.css.AlignItems
import com.varabyte.kobweb.compose.css.JustifyContent
import com.varabyte.kobweb.compose.ui.*
import com.varabyte.kobweb.compose.ui.modifiers.*
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div

@Composable
fun BSCard(
    modifier: Modifier = Modifier,
    id: String? = null,
    alignment: Alignment = Alignment.TopStart,
    shadow: Boolean = true,
    borderRadius: BSBorderRadius? = null,
    customization: CardCustomization? = null,
    content: @Composable () -> Unit
) {
    val randomId = remember {
        id ?: UniqueIdGenerator.generateUniqueId("card")
    }

    Div(
        attrs = Modifier
            .then(modifier)
            .id(randomId)
            .classNames("card")
            .thenIf(
                condition = shadow,
                other = Modifier.classNames("shadow")
            )
            .thenIf(
                condition = customization == null && borderRadius != null,
                other = borderRadius?.let {
                    Modifier.borderRadius(
                        topLeft = it.topLeft,
                        topRight = it.topRight,
                        bottomRight = it.bottomRight,
                        bottomLeft = it.bottomLeft
                    )
                } ?: Modifier
            )
            .styleModifier {
                if (customization != null) {
                    customization.backgroundColor?.let { property("background-color", it) }
                    customization.borderColor?.let { property("border-color", it) }
                    customization.borderWidth?.let { property("border-width", it) }
                    property("border-radius", customization.borderRadius)
                    customization.padding?.let { property("padding", it) }
                    customization.margin?.let { property("margin", it) }
                    customization.width?.let { property("width", it) }
                    customization.height?.let { property("height", it) }
                }
            }
            .display(DisplayStyle.Flex)
            .flexDirection(FlexDirection.Column)
            .justifyContent(alignment.toJustifyContent())
            .alignItems(alignment.toAlignItems())
            .toAttrs()
    ) {
        content()
    }
}

// Helper functions to convert Alignment to Justify Content and Align Items
private fun Alignment.toJustifyContent(): JustifyContent = when (this) {
    Alignment.TopStart, Alignment.CenterStart, Alignment.BottomStart -> JustifyContent.FlexStart
    Alignment.TopCenter, Alignment.Center, Alignment.BottomCenter -> JustifyContent.Center
    Alignment.TopEnd, Alignment.CenterEnd, Alignment.BottomEnd -> JustifyContent.FlexEnd
    Alignment.FromStyle -> JustifyContent.SpaceBetween
}

private fun Alignment.toAlignItems(): AlignItems = when (this) {
    Alignment.TopStart, Alignment.TopCenter, Alignment.TopEnd -> AlignItems.FlexStart
    Alignment.CenterStart, Alignment.Center, Alignment.CenterEnd -> AlignItems.Center
    Alignment.BottomStart, Alignment.BottomCenter, Alignment.BottomEnd -> AlignItems.FlexEnd
    Alignment.FromStyle -> AlignItems.Normal
}

// Data class for custom card styling
data class CardCustomization(
    val backgroundColor: String? = null,
    val borderColor: String? = null,
    val borderWidth: String? = null,
    val borderRadius: String = "0.25rem",
    val padding: String? = null,
    val margin: String? = null,
    val width: String? = null,
    val height: String? = null
)

// Reusing BSBorderRadius from your example
data class BSBorderRadius(
    val topLeft: CSSLengthOrPercentageValue,
    val topRight: CSSLengthOrPercentageValue,
    val bottomRight: CSSLengthOrPercentageValue,
    val bottomLeft: CSSLengthOrPercentageValue
){
    constructor(all:CSSLengthOrPercentageValue) : this(all,all,all,all)
}
naser09 commented 2 months ago

one more thing . with the default file picker I wasn't able to do what I wanted because the file is plain text and it got encoded to base64 . so for picking different file I modified BSFilePicker to changed the on click function for file pick . . document.loadTextFromDisk( accept = accept, onLoad = { onFileSelected(filename, it) placeholderText = filename } )

here is the complete code for text picker .


@Composable
fun BSTextFilePicker(
    modifier: Modifier = Modifier,
    id: String? = null,
    label: String? = null,
    placeholder: String = "No file selected.",
    size: InputSize = InputSize.Default,
    disabled: Boolean = false,
    accept: String = "plain/text, .txt",
    onFileSelected: (String, String) -> Unit
) {
    val randomId = remember {
        id ?: UniqueIdGenerator.generateUniqueId("fileinput")
    }
    var placeholderText by remember { mutableStateOf(placeholder) }
    Div(attrs = modifier.toAttrs()) {
        if(label != null) {
            Label(
                attrs = Modifier
                    .classNames("form-label")
                    .toAttrs(),
                forId = randomId
            )
            {
                Text(value = label)
            }
        }
        Row(
            modifier = Modifier
                .id(randomId)
                .thenIf(
                    condition = disabled,
                    // TODO: Will this color get used anywhere? Should we extract it into a constant?
                    other = Modifier.backgroundColor(Color.rgb(0xFAFAFA))
                )
                .border(
                    width = 1.px,
                    style = LineStyle.Solid,
                    color = rgba(r = 206, g = 212, b = 218, a = 1)
                )
                .padding(all = 0.px)
                .onClick {
                    if (!disabled) {
                        document.loadTextFromDisk(
                            accept = accept,
                            onLoad = {
                                onFileSelected(filename, it)
                                placeholderText = filename
                            }
                        )
                    }
                }
                .overflow(Overflow.Hidden)
                .textOverflow(TextOverflow.Ellipsis)
                .thenIf(
                    condition = size != InputSize.Default,
                    other = Modifier.classNames(size.value)
                )
                .borderRadius(0.375.cssRem),
            verticalAlignment = Alignment.CenterVertically,
            horizontalArrangement = Arrangement.Start
        ) {
            BSButton(
                modifier = Modifier
                    .margin(all = 0.px)
                    .borderRadius(topRight = 0.px, bottomRight = 0.px),
                text = "Browse...",
                variant = ButtonVariant.Light,
                size = when (size) {
                    InputSize.Default -> {
                        ButtonSize.Default
                    }

                    InputSize.Small -> {
                        ButtonSize.Small
                    }

                    InputSize.Large -> {
                        ButtonSize.Large
                    }
                },
                disabled = disabled,
                onClick = {}
            )
            SpanText(
                modifier = Modifier
                    .thenIf(
                        condition = disabled,
                        other = Modifier.classNames("text-muted")
                    )
                    .margin(leftRight = 12.px),
                text = placeholderText
            )
        }
    }
}```