Replace external bootsrap dependency with a copy

This commit is contained in:
Alexander Nozik 2024-04-20 12:13:43 +03:00
parent 51bb46a45c
commit fd5ff5e30c
59 changed files with 4910 additions and 95 deletions

View File

@ -10,7 +10,7 @@ val dataforgeVersion by extra("0.8.0")
allprojects {
group = "space.kscience"
version = "0.4.1"
version = "0.4.2-dev-2"
}
subprojects {
@ -25,8 +25,8 @@ subprojects {
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions {
freeCompilerArgs = freeCompilerArgs + "-Xcontext-receivers"
compilerOptions {
freeCompilerArgs.add("-Xcontext-receivers")
}
}

View File

@ -3,8 +3,8 @@
package space.kscience.visionforge.gdml.demo
import androidx.compose.runtime.*
import app.softwork.bootstrapcompose.Container
import app.softwork.bootstrapcompose.Icon
import bootstrap.Container
import bootstrap.Icon
import org.jetbrains.compose.web.ExperimentalComposeWebApi
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.attributes.name

View File

@ -1,8 +1,8 @@
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import app.softwork.bootstrapcompose.Column
import app.softwork.bootstrapcompose.Row
import bootstrap.Column
import bootstrap.Row
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import org.jetbrains.compose.web.css.*

View File

@ -1,65 +1,54 @@
plugins {
id("space.kscience.gradle.mpp")
alias(spclibs.plugins.compose)
// alias(spclibs.plugins.ktor)
application
}
group = "ru.mipt.npm"
val ktorVersion: String = spclibs.versions.ktor.get()
kscience {
useCoroutines()
useSerialization()
useKtor()
fullStack(
"muon-monitor.js",
jvmConfig = { withJava() },
// jsConfig = { useCommonJs() },
jvmConfig = {withJava()},
browserConfig = {
webpackTask{
cssSupport{
commonWebpackConfig {
cssSupport {
enabled = true
}
scssSupport{
scssSupport {
enabled = true
}
}
}
)
useCoroutines()
useSerialization()
useKtor()
commonMain {
implementation(projects.visionforgeSolid)
implementation(projects.visionforgeComposeHtml)
}
jvmMain {
implementation("org.apache.commons:commons-math3:3.6.1")
implementation("io.ktor:ktor-server-cio:${ktorVersion}")
implementation("io.ktor:ktor-server-content-negotiation:${ktorVersion}")
implementation("io.ktor:ktor-serialization-kotlinx-json:${ktorVersion}")
implementation("ch.qos.logback:logback-classic:1.2.11")
implementation("io.ktor:ktor-server-cio")
implementation("io.ktor:ktor-server-content-negotiation")
implementation("io.ktor:ktor-serialization-kotlinx-json")
implementation(spclibs.logback.classic)
}
jsMain {
implementation(projects.visionforgeThreejs)
//implementation(devNpm("webpack-bundle-analyzer", "4.4.0"))
}
}
kotlin{
kotlin {
explicitApi = null
}
application {
mainClass.set("ru.mipt.npm.muon.monitor.server.MMServerKt")
}
//distributions {
// main {
// contents {
// from("$buildDir/libs") {
// rename("${rootProject.name}-jvm", rootProject.name)
// into("lib")
// }
// }
// }
//}
mainClass.set("ru.mipt.npm.muon.monitor.MMServerKt")
}

View File

@ -3,16 +3,15 @@ package ru.mipt.npm.muon.monitor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember
import app.softwork.bootstrapcompose.Button
import app.softwork.bootstrapcompose.ButtonGroup
import app.softwork.bootstrapcompose.Color.Secondary
import app.softwork.bootstrapcompose.Container
import app.softwork.bootstrapcompose.Layout.Width
import bootstrap.Button
import bootstrap.ButtonGroup
import bootstrap.Layout.Width.Full
import kotlinx.browser.window
import kotlinx.coroutines.await
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.P
import org.jetbrains.compose.web.dom.Span
import org.jetbrains.compose.web.dom.Text
@ -52,8 +51,9 @@ fun MMApp(context: Context, model: Model, selected: Name? = null) {
val events = remember { mutableStateListOf<Event>() }
Container(fluid = true,
Div(
attrs = {
classes("container-fluid")
style {
height(100.vh - 12.pt)
}
@ -66,7 +66,7 @@ fun MMApp(context: Context, model: Model, selected: Name? = null) {
options = mmOptions,
sidebarTabs = {
Tab("Events") {
ButtonGroup({ Layout.width = Width.Full }) {
ButtonGroup({ Layout.width = Full }) {
Button("Next") {
context.launch {
val event = window.fetch(
@ -85,7 +85,7 @@ fun MMApp(context: Context, model: Model, selected: Name? = null) {
model.displayEvent(event)
}
}
Button("Clear", color = Secondary) {
Button("Clear", color = bootstrap.Color.Secondary) {
events.clear()
model.reset()
}

View File

@ -1,4 +1,4 @@
package ru.mipt.npm.muon.monitor.server
package ru.mipt.npm.muon.monitor
import io.ktor.http.ContentType
@ -17,7 +17,6 @@ import io.ktor.server.response.respondText
import io.ktor.server.routing.Routing
import io.ktor.server.routing.get
import org.apache.commons.math3.random.JDKRandomGenerator
import ru.mipt.npm.muon.monitor.Model
import ru.mipt.npm.muon.monitor.sim.Cos2TrackGenerator
import ru.mipt.npm.muon.monitor.sim.simulateOne
import space.kscience.dataforge.context.Context

View File

@ -12,7 +12,7 @@ repositories {
}
kotlin {
jvmToolchain(11)
js(IR) {
browser {
webpackTask {
@ -30,12 +30,9 @@ kotlin {
jvm {
// withJava()
compilations.all {
kotlinOptions {
jvmTarget = "11"
freeCompilerArgs =
freeCompilerArgs + "-Xjvm-default=all" + "-Xopt-in=kotlin.RequiresOptIn" + "-Xlambdas=indy" + "-Xcontext-receivers"
}
compilerOptions {
freeCompilerArgs.addAll("-Xjvm-default=all", "-Xopt-in=kotlin.RequiresOptIn", "-Xlambdas=indy", "-Xcontext-receivers")
}
testRuns["test"].executionTask.configure {
useJUnitPlatform()

View File

@ -1,12 +1,7 @@
kotlin.code.style=official
kotlin.mpp.stability.nowarn=true
kotlin.js.compiler=ir
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx4G
org.jetbrains.compose.experimental.jscanvas.enabled=true
toolsVersion=0.15.2-kotlin-2.0.0-Beta5
#kotlin.experimental.tryK2=true
#kscience.wasm.disabled=true
toolsVersion=0.15.2-kotlin-2.0.0-RC1

View File

@ -18,7 +18,6 @@ pluginManagement {
id("space.kscience.gradle.project") version toolsVersion
id("space.kscience.gradle.mpp") version toolsVersion
id("space.kscience.gradle.jvm") version toolsVersion
id("space.kscience.gradle.js") version toolsVersion
}
}

View File

@ -0,0 +1,21 @@
# Module visionforge-compose-html
## Usage
## Artifact:
The Maven coordinates of this project are `space.kscience:visionforge-compose-html:0.4.1`.
**Gradle Kotlin DSL:**
```kotlin
repositories {
maven("https://repo.kotlin.link")
mavenCentral()
}
dependencies {
implementation("space.kscience:visionforge-compose-html:0.4.1")
}
```

View File

@ -19,12 +19,11 @@ kotlin {
}
}
jsMain{
jsMain {
dependencies {
api("app.softwork:bootstrap-compose:0.1.15")
api("app.softwork:bootstrap-compose-icons:0.1.15")
implementation(compose.html.svg)
implementation(npm("bootstrap", "5.3.3"))
implementation(npm(" bootstrap-icons", "1.11.3"))
api("com.benasher44:uuid:0.8.4")
api(compose.html.core)
}
}

View File

@ -0,0 +1,57 @@
package bootstrap
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.dom.A
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.ContentBuilder
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLAnchorElement
import org.w3c.dom.HTMLDivElement
@Composable
public fun Alert(
color: Color,
dismissible: Boolean = true,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLDivElement>? = null,
content: @Composable () -> Unit
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Div({
classes("alert", "alert-$color")
if (dismissible) {
needsJS
classes("alert-dismissible")
}
if (classes != null) {
classes(classes = classes)
}
attr("role", "alert")
attrs?.invoke(this)
}) {
content()
}
}
@Composable
public fun Link(
href: String?,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLAnchorElement>? = null,
content: ContentBuilder<HTMLAnchorElement>
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
A(href, {
classes("alert-link")
if (classes != null) {
classes(classes = classes)
}
attrs?.invoke(this)
}, content)
}

View File

@ -0,0 +1,204 @@
package bootstrap
import androidx.compose.runtime.*
import kotlinx.browser.window
import org.jetbrains.compose.web.attributes.builders.InputAttrsScope
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.*
import org.w3c.dom.Element
import org.w3c.dom.HTMLButtonElement
import org.w3c.dom.HTMLDivElement
import org.w3c.dom.events.Event
import org.w3c.dom.events.EventTarget
private class ReferenceHolder<T>(var ref: T? = null)
/**
* A TextInput widget with autocomplete/search suggestions that will display below the TextField. This component
* only supports the "controlled" Input variant. This version provides limited support and expects the user to
* listen to all input change events and build the suggestions list.
*
* @param inputAttrs attributes builder.
* @param suggestions Composable to build the list of suggestions to display
*/
@Composable
public fun Autocomplete(
inputAttrs: InputAttrsScope<String>.() -> Unit = {},
suggestions: ContentBuilder<HTMLDivElement>? = null
) {
var itemsVisible by remember { mutableStateOf(false) }
val parentElement = remember { ReferenceHolder<Element>() }
DisposableEffect(true) {
val effect: (Event) -> Unit = { event ->
if (parentElement.ref?.isChild(event.target) != true) {
itemsVisible = false
}
}
window.document.addEventListener("mousedown", effect)
onDispose {
window.document.removeEventListener("mousedown", effect)
}
}
Div(
attrs = {
style {
padding(0.px)
}
}
) {
DisposableEffect(true) {
parentElement.ref = scopeElement
onDispose { parentElement.ref = null }
}
TextInput(attrs = {
inputAttrs()
onInput {
itemsVisible = true
}
})
Div(
attrs = {
Style
classes("dropdown-menu")
if (itemsVisible) {
classes("d-block")
} else {
classes("d-none")
}
onClick {
itemsVisible = false
}
style {
padding(0.px)
}
},
content = suggestions
)
}
}
/**
* An AutoComplete that displays Strings as suggestions.
*
* @param value The value for the TextInput
* @param onValueChange callback when the user changes the Input text
* @param inputId The Id to assign to the TextInput
* @param suggestions List of suggestions to be presented to the user
* @param onSelection callback for when the user selects a suggestion. The TextInput value will not be updated until
* this Composable is recomposed with an updated value.
*/
@Composable
public fun Autocomplete(
value: String,
onValueChange: (String) -> Unit,
inputId: String? = null,
suggestions: List<String> = listOf(),
onSelection: (String) -> Unit
) {
Autocomplete(
value = value,
onValueChange = onValueChange,
inputId = inputId,
suggestions = suggestions,
content = { _, item ->
Text(item)
},
onSelection = { _, item -> onSelection(item) }
)
}
/**
* A TextInput widget with autocomplete/search suggestions that will display below the TextField. This component
* only supports the "controlled" Input variant. It uses a callback for displaying suggestion items.
*
* @param value The value for the TextInput
* @param onValueChange callback when the user changes the Input text
* @param inputId The Id to assign to the TextInput
* @param suggestions List of suggestions to be presented to the user
* @param content Composable for rendering item content. The functions first parameter is the items index and the 2nd
* parameter is the item to be rendered.
* @param onSelection callback for when the user selects a suggestion. The TextInput value will not be updated until
* this Composable is recomposed with an updated value.
*
* This function would be better if using a generic type for the List items, content Composable callback parameter,
* and onSelection callback, except 2 issues may be responsible for compiler failures:
* https://github.com/JetBrains/compose-jb/issues/774
* https://github.com/JetBrains/compose-jb/issues/1226
*/
@Composable
public fun Autocomplete(
value: String,
onValueChange: (String) -> Unit,
inputId: String? = null,
suggestions: List<String> = listOf(),
content: @Composable ElementScope<HTMLButtonElement>.(index: Int, item: String) -> Unit,
onSelection: (index: Int, item: String) -> Unit
) {
Autocomplete(inputAttrs = {
inputId?.let { id(inputId) }
value(value)
onInput {
onValueChange(it.value)
}
}) {
Ul(attrs = {
style {
padding(0.px)
margin(0.px)
listStyle("none")
}
}) {
suggestions.forEachIndexed { index, item ->
Li(attrs = {
style {
padding(0.cssRem)
}
}) {
Button(attrs = {
classes("dropdown-item")
onClick {
onSelection(index, item)
}
}) {
content(index, item)
}
if (index < suggestions.lastIndex) {
Hr(attrs = {
style {
margin(0.px)
}
})
}
}
}
}
}
}
/**
* Traverses the element tree to see if this Element is a parent of the provided element.
* @param element Element to check if it is a child of this Element
*/
private fun Element.isChild(element: Element): Boolean {
var nextElement: Element? = element
while (nextElement != this && nextElement != null) {
nextElement = nextElement.parentElement
}
return nextElement == this
}
/**
* Traverses the element tree to see if this Element is a parent of the provided EventTarget.
* @param eventTarget EventTarget to check if it is a child of this Element
*/
private fun Element.isChild(eventTarget: EventTarget?): Boolean {
return if (eventTarget is Element) {
isChild(eventTarget)
} else {
false
}
}

View File

@ -0,0 +1,36 @@
package bootstrap
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.ContentBuilder
import org.jetbrains.compose.web.dom.Span
import org.w3c.dom.HTMLSpanElement
@Composable
public fun Badge(
backgroundColor: Color,
textColor: Color? = null,
round: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLSpanElement>? = null,
content: ContentBuilder<HTMLSpanElement>
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Span(attrs = {
classes("badge", backgroundColor.background())
if (textColor != null) {
classes(textColor.text())
}
if (round) {
classes("rounded-pill")
}
if (classes != null) {
classes(classes = classes)
}
attrs?.invoke(this)
}, content)
}

View File

@ -0,0 +1,26 @@
package bootstrap
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.ContentBuilder
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLDivElement
@Composable
public fun Box(
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLDivElement>? = null,
content: ContentBuilder<HTMLDivElement>? = null
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Div(attrs = {
if (classes != null) {
classes(classes = classes)
}
attrs?.invoke(this)
}, content = content)
}

View File

@ -0,0 +1,27 @@
package bootstrap
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.ContentBuilder
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLDivElement
@Composable
public fun Brand(
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLDivElement>? = null,
content: ContentBuilder<HTMLDivElement>
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Div(attrs = {
classes("navbar-brand")
if (classes != null) {
classes(classes = classes)
}
attrs?.invoke(this)
}, content)
}

View File

@ -0,0 +1,29 @@
package bootstrap
import org.jetbrains.compose.web.css.CSSLengthValue
import org.jetbrains.compose.web.css.px
public enum class Breakpoint(private val classInfix: String) {
Small("sm"),
Medium("md"),
Large("lg"),
ExtraLarge("xl"),
ExtraExtraLarge("xxl");
override fun toString(): String = classInfix
}
/**
* Breakpoint thresholds that match Bootstrap defaults. If the defaults were overridden in the
* css (e.g. using sass to generate a custom Bootstrap css), then this variable should be updated with a
* new map that matches before using classes/functions that create
* new styles in a media query. If this is not done, the breakpoints won't be consistent with modified
* bootstrap css classes.
*/
public var breakpoints: Map<Breakpoint, CSSLengthValue> = mapOf(
Breakpoint.Small to 576.px,
Breakpoint.Medium to 768.px,
Breakpoint.Large to 992.px,
Breakpoint.ExtraLarge to 1200.px,
Breakpoint.ExtraExtraLarge to 1400.px,
)

View File

@ -0,0 +1,95 @@
package bootstrap
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.attributes.ButtonType
import org.jetbrains.compose.web.attributes.disabled
import org.jetbrains.compose.web.attributes.type
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.Button
import org.jetbrains.compose.web.dom.ContentBuilder
import org.jetbrains.compose.web.dom.Text
import org.w3c.dom.HTMLButtonElement
@Composable
public fun Button(
title: String,
color: Color = Color.Primary,
size: ButtonSize = ButtonSize.Default,
outlined: Boolean = false,
type: ButtonType = ButtonType.Submit,
disabled: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLButtonElement>? = null,
action: () -> Unit
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Button(attrs = {
classes("btn")
classes(size.toString())
if (outlined) {
classes("btn-outline-$color")
} else {
classes("btn-$color")
}
if (disabled) {
disabled()
}
if (classes != null) {
classes(classes = classes)
}
attrs?.invoke(this)
type(type)
onClick {
action()
}
}) {
Text(title)
}
}
public enum class ButtonSize(private val prefix: String) {
Default("btn"), Large("btn-lg"), Small("btn-sm");
override fun toString(): String = prefix
}
@Composable
public fun Button(
content: ContentBuilder<HTMLButtonElement>,
color: Color = Color.Primary,
outlined: Boolean = false,
type: ButtonType = ButtonType.Submit,
disabled: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLButtonElement>? = null,
action: () -> Unit
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Button(attrs = {
classes("btn")
if (outlined) {
classes("btn-outline-$color")
} else {
classes("btn-$color")
}
if (disabled) {
disabled()
}
if (classes != null) {
classes(classes = classes)
}
attrs?.invoke(this)
type(type)
onClick {
action()
}
}, content)
}

View File

@ -0,0 +1,24 @@
package bootstrap
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.dom.ContentBuilder
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLDivElement
@Composable
public fun ButtonGroup(
styling: (Styling.() -> Unit)? = null,
content: ContentBuilder<HTMLDivElement>
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Div({
classes("btn-group")
if (classes != null) {
classes(classes = classes)
}
}, content)
}

View File

@ -0,0 +1,49 @@
package bootstrap
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.ContentBuilder
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLDivElement
@Composable
public fun Card(
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLDivElement>? = null,
headerAttrs: AttrBuilderContext<HTMLDivElement>? = null,
header: ContentBuilder<HTMLDivElement>? = null,
footerAttrs: AttrBuilderContext<HTMLDivElement>? = null,
footer: ContentBuilder<HTMLDivElement>? = null,
bodyAttrs: AttrBuilderContext<HTMLDivElement>? = null,
body: ContentBuilder<HTMLDivElement>
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Div({
classes("card")
if (classes != null) {
classes(classes = classes)
}
attrs?.invoke(this)
}) {
header?.let {
Div({
classes("card-header")
headerAttrs?.invoke(this)
}, header)
}
Div({
classes("card-body")
bodyAttrs?.invoke(this)
}, body)
footer?.let {
Div({
classes("card-footer")
footerAttrs?.invoke(this)
}, footer)
}
}
}

View File

@ -0,0 +1,63 @@
package bootstrap
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.benasher44.uuid.uuid4
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.attributes.builders.InputAttrsScope
import org.jetbrains.compose.web.attributes.disabled
import org.jetbrains.compose.web.attributes.forId
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Input
import org.jetbrains.compose.web.dom.Label
import org.jetbrains.compose.web.dom.Text
@Composable
public fun Checkbox(
checked: Boolean,
label: String,
id: String = remember { uuid4().toString() },
disabled: Boolean = false,
inline: Boolean = false,
switch: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrs: (InputAttrsScope<Boolean>.() -> Unit)? = null,
onClick: (Boolean) -> Unit
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Div({
classes(BSClasses.formCheck)
if (classes != null) {
classes(classes = classes)
}
if (inline) {
classes(BSClasses.formCheckInline)
}
if (switch) {
classes(BSClasses.formSwitch)
}
}) {
Input(attrs = {
classes(BSClasses.formCheckInput)
id("_$id")
checked(checked)
onInput { event ->
onClick(event.value)
}
if (disabled) {
disabled()
}
attrs?.invoke(this)
}, type = InputType.Checkbox)
Label(attrs = {
classes(BSClasses.formCheckLabel)
forId("_$id")
}) {
Text(label)
}
}
}

View File

@ -0,0 +1,99 @@
package bootstrap
public object BSClasses {
public const val disabled: String = "disabled"
public const val active: String = "active"
public val align: Align = Align
public val display: Display = Display
public const val colFormLabel: String = "col-form-label"
public const val collapse: String = "collapse"
public const val formCheck: String = "form-check"
public const val formCheckInline: String = "form-check-inline"
public const val formCheckInput: String = "form-check-input"
public const val formCheckLabel: String = "form-check-label"
public const val formControl: String = "form-control"
public const val formFloating: String = "form-floating"
public const val formLabel: String = "form-label"
public const val formRange: String = "form-range"
public const val formSelect: String = "form-select"
public const val formSelectSmall: String = "form-select-sm"
public const val formSelectLarge: String = "form-select-lg"
public const val formSwitch: String = "form-switch"
public const val formText: String = "form-text"
public const val inputGroup: String = "input-group"
public const val inputGroupLarge: String = "input-group-lg"
public const val inputGroupSmall: String = "input-group-sm"
public const val inputGroupText: String = "input-group-text"
public const val listGroup: String = "list-group"
public const val listGroupFlush: String = "list-group-flush"
public const val listGroupHorizontal: String = "list-group-horizontal"
public const val listGroupItem: String = "list-group-item"
public const val listGroupItemAction: String = "list-group-item-action"
public const val listGroupNumbered: String = "list-group-numbered"
public const val navbar: String = "navbar"
public const val navbarBrand: String = "navbar-brand"
public const val navbarCollapse: String = "navbar-collapse"
public const val navLink: String = "nav-link"
public const val navbarNav: String = "navbar-nav"
public const val navbarToggler: String = "navbar-toggler"
public const val navbarTogglerIcon: String = "navbar-toggler-icon"
}
public object Align {
public const val baseline: String = "align-baseline"
public const val top: String = "align-top"
public const val middle: String = "align-middle"
public const val bottom: String = "align-bottom"
public const val textTop: String = "align-text-top"
public const val textBottom: String = "align-text-bottom"
}
public object Display {
public val small: DisplayBreakpoint = DisplayBreakpoint(Breakpoint.Small)
public val medium: DisplayBreakpoint = DisplayBreakpoint(Breakpoint.Medium)
public val extraLarge: DisplayBreakpoint = DisplayBreakpoint(Breakpoint.ExtraLarge)
public val extraExtraLarge: DisplayBreakpoint = DisplayBreakpoint(Breakpoint.ExtraExtraLarge)
public const val none: String = "d-${DisplayValue.none}"
public const val inline: String = "d-${DisplayValue.inline}"
public const val inlineBlock: String = "d-${DisplayValue.inlineBlock}"
public const val block: String = "d-${DisplayValue.block}"
public const val grid: String = "d-${DisplayValue.grid}"
public const val table: String = "d-${DisplayValue.table}"
public const val tableCell: String = "d-${DisplayValue.tableCell}"
public const val tableRow: String = "d-${DisplayValue.tableRow}"
public const val flex: String = "d-${DisplayValue.flex}"
public const val inlineFlex: String = "d-${DisplayValue.inlineFlex}"
}
public class DisplayBreakpoint(breakpoint: Breakpoint) {
public val none: String = makeDisplayClass(breakpoint, DisplayValue.none)
public val inline: String = makeDisplayClass(breakpoint, DisplayValue.inline)
public val inlineBlock: String = makeDisplayClass(breakpoint, DisplayValue.inlineBlock)
public val block: String = makeDisplayClass(breakpoint, DisplayValue.block)
public val grid: String = makeDisplayClass(breakpoint, DisplayValue.grid)
public val table: String = makeDisplayClass(breakpoint, DisplayValue.table)
public val tableCell: String = makeDisplayClass(breakpoint, DisplayValue.tableCell)
public val tableRow: String = makeDisplayClass(breakpoint, DisplayValue.tableRow)
public val flex: String = makeDisplayClass(breakpoint, DisplayValue.flex)
public val inlineFlex: String = makeDisplayClass(breakpoint, DisplayValue.inlineFlex)
}
private fun makeDisplayClass(breakpoint: Breakpoint, value: String): String {
return "d-$breakpoint-$value"
}
private object DisplayValue {
const val none = "none"
const val inline = "inline"
const val inlineBlock = "inline-block"
const val block = "block"
const val grid = "grid"
const val table = "table"
const val tableCell = "table-cell"
const val tableRow = "table-row"
const val flex = "flex"
const val inlineFlex = "inline-flex"
}

View File

@ -0,0 +1,26 @@
package bootstrap
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.attributes.ButtonType
import org.jetbrains.compose.web.attributes.disabled
import org.jetbrains.compose.web.attributes.type
import org.jetbrains.compose.web.dom.Button
@Composable
public fun CloseButton(
disabled: Boolean = false,
onClose: () -> Unit
) {
Style
Button({
type(ButtonType.Button)
classes("btn-close")
attr("aria-label", "Close")
if (disabled) {
disabled()
}
onClick {
onClose()
}
})
}

View File

@ -0,0 +1,36 @@
package bootstrap
import androidx.compose.runtime.Composable
import com.benasher44.uuid.uuid4
import org.jetbrains.compose.web.attributes.ButtonType
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.ContentBuilder
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLButtonElement
import org.w3c.dom.HTMLDivElement
@Composable
public fun Collapse(
title: String,
id: String = uuid4().toString(),
color: Color = Color.Primary,
buttonAttrs: AttrBuilderContext<HTMLButtonElement>? = null,
contentAttrs: AttrBuilderContext<HTMLDivElement>? = null,
content: ContentBuilder<HTMLDivElement>
) {
Style
needsJS
Button(attrs = {
classes("btn", "btn-$color")
attr("data-bs-toggle", "collapse")
attr("data-bs-target", "#_$id")
attr("aria-expanded", "false")
attr("aria-controls", "_$id")
buttonAttrs?.invoke(this)
}, type = ButtonType.Button, title = title) { }
Div({
classes("collapse")
id("_$id")
contentAttrs?.invoke(this)
}, content)
}

View File

@ -0,0 +1,23 @@
package bootstrap
public enum class Color(private val value: String) {
Primary("primary"),
Secondary("secondary"),
Success("success"),
Info("info"),
Warning("warning"),
Danger("danger"),
Light("light"),
Dark("dark"),
Body("body"),
Muted("muted"),
White("white"),
Black50("black-50"),
White50("white-50"),
Transparent("transparent");
override fun toString(): String = value
public fun background(): String = "bg-$value"
public fun text(): String = "text-$value"
}

View File

@ -0,0 +1,33 @@
package bootstrap
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.ContentBuilder
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLDivElement
@Composable
public fun Column(
auto: Boolean = false,
breakpoint: Breakpoint? = null,
size: Int? = null,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLDivElement>? = null,
content: ContentBuilder<HTMLDivElement>
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Div(attrs = {
classes("col" + (breakpoint?.let { "-$it" } ?: "") + (size?.let { "-$it" } ?: ""))
if (classes != null) {
classes(classes = classes)
}
if (auto) {
classes("col-auto")
}
attrs?.invoke(this)
}, content)
}

View File

@ -0,0 +1,40 @@
package bootstrap
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.ContentBuilder
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLDivElement
@Composable
public fun Container(
fluid: Boolean = false,
type: Breakpoint? = null,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLDivElement>? = null,
content: ContentBuilder<HTMLDivElement>?
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Div(attrs = {
if (classes != null) {
classes(classes = classes)
}
when {
fluid -> {
classes("container-fluid")
}
type != null -> {
classes("container-$type")
}
else -> {
classes("container")
}
}
attrs?.invoke(this)
}, content)
}

View File

@ -0,0 +1,194 @@
package bootstrap
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.benasher44.uuid.uuid4
import org.jetbrains.compose.web.attributes.AttrsScope
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.dom.Button
import org.w3c.dom.Element
import org.w3c.dom.HTMLLIElement
import org.w3c.dom.HTMLUListElement
public class DropDownBuilder(private val scope: ElementScope<HTMLUListElement>) : ElementScope<HTMLUListElement> by scope {
@Composable
public fun Button(title: String, styling: (Styling.() -> Unit)? = null, onClick: () -> Unit) {
Li {
val buttonClasses = styling?.let {
Styling().apply(it).generate()
}
Button({
classes("dropdown-item")
if (buttonClasses != null) {
classes(classes = buttonClasses)
}
onClick { onClick() }
}) {
Text(title)
}
}
}
@Composable
public fun Divider(styling: (Styling.() -> Unit)? = null) {
Li {
val buttonClasses = styling?.let {
Styling().apply(it).generate()
}
Hr {
classes("dropdown-divider")
if (buttonClasses != null) {
classes(classes = buttonClasses)
}
}
}
}
@Composable
public fun Header(title: String, styling: (Styling.() -> Unit)? = null) {
Li {
val buttonClasses = styling?.let {
Styling().apply(it).generate()
}
H6({
classes("dropdown-header")
if (buttonClasses != null) {
classes(buttonClasses)
}
}) {
Text(title)
}
}
}
@Composable
public fun Custom(block: ContentBuilder<HTMLLIElement>) {
Li {
block()
}
}
}
public object DropDown {
public enum class Direction(private val classname: String) {
Down("dropdown"),
Up("dropup"),
Right("dropend"),
Left("dropstart");
public override fun toString(): String {
return classname
}
}
public sealed class MenuAlignment(internal vararg val classes: String) {
public object Start : MenuAlignment("dropdown-menu")
public object End : MenuAlignment("dropdown-menu", "dropdown-menu-end")
public class StartThenEndAtBreakpoint(breakpoint: Breakpoint) :
MenuAlignment("dropdown-menu", "dropdown-menu-$breakpoint-end")
public class EndThenStartAtBreakpoint(breakpoint: Breakpoint) :
MenuAlignment("dropdown-menu", "dropdown-menu-end", "dropdown-menu-$breakpoint-start")
}
}
@Composable
public fun DropDown(
title: String,
id: String = remember { "dropdownMenu${uuid4()}" },
size: ButtonSize = ButtonSize.Default,
color: Color = Color.Primary,
styling: (Styling.() -> Unit)? = null,
direction: DropDown.Direction = DropDown.Direction.Down,
menuAlignment: DropDown.MenuAlignment = DropDown.MenuAlignment.Start,
block: @Composable DropDownBuilder.() -> Unit,
) {
Style
needsJS
needsPopper
val trigger = @Composable { classes: List<String>? ->
Button(attrs = {
classes("btn", "btn-$color", "dropdown-toggle")
classes(size.toString())
if (classes != null) {
classes(classes)
}
id(id)
attr("data-bs-toggle", "dropdown")
attr("aria-expanded", "false")
responsiveAlignmentAttribute(menuAlignment)
}) {
Text(title)
}
}
DropDownBase(trigger, id, styling, direction, menuAlignment, block)
}
@Composable
public fun NavbarDropDown(
title: String,
href: String?,
id: String = remember { "dropdownMenu${uuid4()}" },
styling: (Styling.() -> Unit)? = null,
direction: DropDown.Direction = DropDown.Direction.Down,
menuAlignment: DropDown.MenuAlignment = DropDown.MenuAlignment.Start,
block: @Composable DropDownBuilder.() -> Unit,
) {
Style
needsJS
val trigger = @Composable { classes: List<String>? ->
A(
attrs = {
classes("nav-link", "dropdown-toggle")
if (classes != null) {
classes(classes)
}
id(id)
attr("data-bs-toggle", "dropdown")
attr("aria-expanded", "false")
responsiveAlignmentAttribute(menuAlignment)
},
href = href
) {
Text(title)
}
}
DropDownBase(trigger, id, styling, direction, menuAlignment, block)
}
private fun <T : Element> AttrsScope<T>.responsiveAlignmentAttribute(menuAlignment: DropDown.MenuAlignment) {
if (menuAlignment is DropDown.MenuAlignment.EndThenStartAtBreakpoint ||
menuAlignment is DropDown.MenuAlignment.StartThenEndAtBreakpoint
) {
attr("data-bs-display", "static")
}
}
@Composable
private fun DropDownBase(
triggerElement: @Composable (classes: List<String>?) -> Unit,
id: String,
styling: (Styling.() -> Unit)? = null,
direction: DropDown.Direction,
menuAlignment: DropDown.MenuAlignment,
block: @Composable DropDownBuilder.() -> Unit,
) {
Style
needsJS
val classes = styling?.let {
Styling().apply(it).generate()
}
Div({ classes("btn-group", direction.toString()) }) {
triggerElement(classes)
Ul({
classes(classes = menuAlignment.classes)
attr("aria-labelledby", id)
}) {
DropDownBuilder(this).block()
}
}
}

View File

@ -0,0 +1,30 @@
package bootstrap
import androidx.compose.runtime.Composable
import androidx.compose.runtime.NonRestartableComposable
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.ContentBuilder
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLDivElement
@Composable
@NonRestartableComposable
public fun FormFloating(
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLDivElement>? = null,
content: ContentBuilder<HTMLDivElement>? = null
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Div(attrs = {
classes("form-floating")
if (classes != null) {
classes(classes = classes)
}
if (attrs != null) {
attrs()
}
}, content)
}

View File

@ -0,0 +1,32 @@
package bootstrap
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.ContentBuilder
import org.jetbrains.compose.web.dom.Label
import org.w3c.dom.HTMLLabelElement
@Composable
public fun FormLabel(
forId: String? = null,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLLabelElement>? = null,
content: ContentBuilder<HTMLLabelElement>? = null
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Label(
forId = forId,
attrs = {
classes(BSClasses.formLabel)
if (classes != null) {
classes(classes = classes)
}
attrs?.invoke(this)
},
content = content
)
}

View File

@ -0,0 +1,773 @@
package bootstrap
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.benasher44.uuid.uuid4
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.*
import org.w3c.dom.HTMLDivElement
import kotlin.reflect.KProperty
@Composable
public fun GridBox(
styling: @Composable GridStyle.() -> Unit,
attrs: AttrBuilderContext<HTMLDivElement>? = null,
content: (@Composable GridContentBuilder.() -> Unit)? = null
) {
Style
val style = GridStyle(styling)
val classes = style.generate()
Div(attrs = {
classes("d-grid")
classes(classes = classes)
attrs?.invoke(this)
}) {
val scope = remember { GridContentBuilder(this) }
content?.invoke(scope)
}
}
public class GridContentBuilder(scope: ElementScope<HTMLDivElement>) : ElementScope<HTMLDivElement> by scope {
public fun Styling.GridItem(spec: GridItemLayout.() -> Unit) {
val s = GridItemLayout().apply(spec)
this.registerGenerator {
s.generate()
}
}
}
@Composable
public fun GridStyle(styling: @Composable GridStyle.() -> Unit): GridStyle = GridStyle().apply { styling() }
public class GridStyle : Styling() {
public val GridLayout: GridLayout = GridLayout()
@Composable
override fun generate(): List<String> {
return super.generate() + GridLayout.generate()
}
}
public class GridLayout {
private var columns: MutableList<GridTemplateTrack> = mutableListOf()
private var rows: MutableList<GridTemplateTrack> = mutableListOf()
private var areas: MutableList<GridArea> = mutableListOf()
public var gap: CSSLengthOrPercentageValue? = null
public var justifyContent: Placement? = null
public var alignContent: Placement? = null
public operator fun invoke(f: GridLayout.() -> Unit) {
this.f()
}
public enum class Placement(private val value: String) {
Start("start"),
End("end"),
Center("center"),
Stretch("stretch"),
SpaceAround("space-around"),
SpaceBetween("space-between"),
SpaceEvenly("space-evenly");
public override fun toString(): String {
return value
}
}
public fun columns(breakpoint: Breakpoint? = null, spec: GridTemplateTrack.() -> Unit) {
columns += GridTemplateTrack(GridTemplateTrack.ColumnOrRow.Column, breakpoint).apply(spec)
}
public fun rows(breakpoint: Breakpoint? = null, spec: GridTemplateTrack.() -> Unit) {
rows += GridTemplateTrack(GridTemplateTrack.ColumnOrRow.Row, breakpoint).apply(spec)
}
public fun areas(breakpoint: Breakpoint? = null, spec: GridArea.() -> Unit) {
areas += GridArea(breakpoint).apply(spec)
}
@Composable
internal fun generate(): List<String> {
val classes: MutableList<String> = mutableListOf()
Style {
val classname = remember { "_${uuid4()}" }
className(classname) style {
gap?.let {
property("gap", gap.toString())
}
justifyContent?.let {
property("justify-content", it.toString())
}
alignContent?.let {
property("align-content", it.toString())
}
}
columns.forEach {
it.apply { classes += generateStyle() }
}
rows.forEach {
it.apply { classes += generateStyle() }
}
areas.forEach {
it.apply { classes += generateStyle() }
}
classes += classname
}
return classes
}
}
public class GridTemplateTrack internal constructor(
private val type: ColumnOrRow,
private val breakpoint: Breakpoint?
) {
private val items: MutableList<Grid.GridTemplateItem> = mutableListOf()
public var gap: CSSLengthOrPercentageValue? = null
public var alignment: Alignment? = null
private var auto = false
internal enum class ColumnOrRow(val gap: String, val align: String) {
Column("column-gap", "align-items"),
Row("row-gap", "justify-items"),
}
public enum class Alignment(private val value: String) {
Start("start"),
End("end"),
Center("center"),
Stretch("stretch");
public override fun toString(): String {
return value
}
}
public fun none() {
check(items.isEmpty()) {
"Cannot add 'none' to already specified grid-template-columns"
}
items.add(Grid.GridTemplateNone)
}
/**
* This function is used to implement grid-template-columns/rows when the
* track list follows the <track-list> syntax.
* See https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns
*/
public fun trackList(block: TrackList.() -> Unit) {
check(items.isEmpty()) {
"Cannot add more tracks"
}
items += TrackList().apply(block).items
}
/**
* This function is used to implement grid-template-columns/rows when the
* track list follows the <auto-track-list> syntax.
* See https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns
*/
public fun autoTrackList(block: AutoTrackList.() -> Unit) {
check(items.isEmpty()) {
"Cannot add more tracks"
}
items += AutoTrackList().apply(block).items
}
/**
* This function is used to implement grid-auto-rows and grid-auto-columns properties.
* See https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns
*/
public fun auto(block: AutoList.() -> Unit) {
check(items.isEmpty()) {
"Cannot add more tracks"
}
items += AutoList().apply(block).items
auto = true
}
@Composable
internal fun StyleSheetBuilder.generateStyle(): String {
val classname = remember { "_${uuid4()}" }
withBreakpoint(breakpoint) {
className(classname) style {
val propValue = items.joinToString(separator = " ")
val propName = when {
auto && type == ColumnOrRow.Column -> "grid-auto-columns"
auto && type == ColumnOrRow.Row -> "grid-auto-rows"
!auto && type == ColumnOrRow.Column -> "grid-template-columns"
!auto && type == ColumnOrRow.Row -> "grid-template-rows"
else -> error("not possible")
}
property(propName, propValue)
gap?.let {
property(type.gap, it)
}
alignment?.let {
property(type.align, it.toString())
}
}
}
return classname
}
}
public open class TrackListUnits {
public val Number.ch: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.ch)
public val Number.em: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.em)
public val Number.ex: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.ex)
public val Number.rem: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.rem)
public val Number.vh: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.vh)
public val Number.vw: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.vw)
public val Number.vmin: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.vmin)
public val Number.vmax: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.vmax)
public val Number.px: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.px)
public val Number.cm: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.cm)
public val Number.mm: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.mm)
public val Number.pc: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.pc)
public val Number.pt: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.pt)
public val Number.percent: Grid.LengthPercentage by LengthPercentageUnitPropertyDelegate(CSSUnit.percent)
public val Number.fr: Grid.Flex
get(): Grid.Flex {
return Grid.Flex(CSSUnitValueTyped(this.toFloat(), CSSUnit.fr))
}
private class LengthPercentageUnitPropertyDelegate<T : CSSUnitLengthOrPercentage>(val unit: T) {
operator fun getValue(thisRef: Number, property: KProperty<*>): Grid.LengthPercentage {
return Grid.LengthPercentage(CSSUnitValueTyped(thisRef.toFloat(), unit))
}
}
}
public class TrackList internal constructor() : TrackListUnits() {
internal val items: MutableList<Grid.TrackListItem> = mutableListOf()
public fun track(names: Array<String>, size: Grid.TrackSizeItem) {
if (names.isNotEmpty()) {
items += Grid.LineNames(names = names)
}
items += size
}
public fun track(size: Grid.TrackSizeItem) {
items += size
}
public fun track(names: Array<String>, repeat: TrackRepeat) {
if (names.isNotEmpty()) {
items += Grid.LineNames(names = names)
}
items += repeat
}
public fun track(repeat: TrackRepeat) {
items += repeat
}
public class TrackRepeat(private val count: Int, private vararg val items: Grid.TrackRepeatItem) :
Grid.TrackListItem {
override fun toString(): String {
return "repeat($count, ${items.joinToString(separator = " ")})"
}
}
public fun lineNames(vararg names: String) {
items += Grid.LineNames(names = names)
}
}
public class AutoList internal constructor() : TrackListUnits() {
internal val items: MutableList<Grid.TrackSizeItem> = mutableListOf()
public fun track(size: Grid.TrackSizeItem) {
items += size
}
}
public data class MinMax(private val min: Grid.InflexibleBreadthItem, private val max: Grid.TrackBreadthItem) :
Grid.TrackSizeItem {
override fun toString(): String {
return "minmax($min,$max)"
}
}
public class AutoTrackList internal constructor() : TrackListUnits() {
internal val items: MutableList<Grid.AutoTrackListItem> = mutableListOf()
private var autoRepeat = false
public fun track(names: List<String>, size: Grid.FixedSizeItem) {
if (names.isNotEmpty()) {
lineNames(names)
}
items += size
}
public fun track(size: Grid.FixedSizeItem) {
items += size
}
public fun track(names: List<String>, repeat: FixedRepeat) {
if (names.isNotEmpty()) {
lineNames(names)
}
items += repeat
}
public fun track(repeat: FixedRepeat) {
items += repeat
}
public fun lineNames(names: List<String>) {
items += Grid.LineNames(names = names.toTypedArray())
}
public fun lineNames(vararg names: String) {
items += Grid.LineNames(*names)
}
public fun autoRepeat(type: RepeatType, vararg repeatItems: Grid.FixedRepeatItem) {
if (type == RepeatType.AutoFill || type == RepeatType.AutoFit) {
require(!autoRepeat) {
"auto-repeat already specified, can only be specified once per grid-template-columns"
}
autoRepeat = true
}
items += AutoRepeat(type, *repeatItems)
}
public class MinMax private constructor(private val min: Any, private val max: Any) : Grid.FixedSizeItem {
public constructor(minimum: Grid.FixedBreadthItem, maximum: Grid.TrackBreadthItem) : this(
min = minimum,
max = maximum
)
public constructor(minimum: Grid.InflexibleBreadthItem, maximum: Grid.FixedBreadthItem) : this(
min = minimum,
max = maximum
)
override fun toString(): String {
return "minmax($min,$max)"
}
}
public class AutoRepeat(private val type: RepeatType, private vararg val items: Grid.FixedRepeatItem) :
Grid.AutoTrackListItem {
override fun toString(): String {
return "repeat($type, ${items.joinToString(separator = " ")})"
}
}
public enum class RepeatType(private val value: String) {
AutoFill("auto-fill"),
AutoFit("auto-fit");
override fun toString(): String {
return value
}
}
public class FixedRepeat(private val count: Int, private vararg val items: Grid.FixedRepeatItem) :
Grid.AutoTrackListItem {
override fun toString(): String {
return "repeat($count, ${items.joinToString(separator = " ")})"
}
}
}
public object Grid {
/**
* Marker interface for items that can be included in a Grid-Template-Columns or
* Grid-Template-Rows propery.
*/
public interface GridTemplateItem
/**
* Marker interface for items that can be included in a <track-list>.
* see https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns
*/
public interface TrackListItem : GridTemplateItem
/**
* Marker interface for items that can be included in an <auto-track-list>.
* see https://developer.mozilla.org/en-US/docs/Web/CSS/grid-template-columns
*/
public interface AutoTrackListItem : GridTemplateItem
// Marker interfaces for items that can be used as a <track-size>
public interface TrackSizeItem : TrackListItem, TrackRepeatItem
public interface TrackBreadthItem : TrackSizeItem
public interface TrackRepeatItem
// Marker interfaces for items that can be used as a <fixed-size>
public interface FixedSizeItem : AutoTrackListItem, FixedRepeatItem
public interface FixedBreadthItem : FixedSizeItem
public interface FixedRepeatItem
public interface InflexibleBreadthItem
internal object GridTemplateNone : GridTemplateItem {
override fun toString(): String {
return "none"
}
}
public class LineNames(private vararg val names: String) :
TrackListItem, AutoTrackListItem, TrackRepeatItem, FixedRepeatItem {
override fun toString(): String {
return names.joinToString(prefix = "[", postfix = "]", separator = " ")
}
}
public data class FitContent(val value: LengthPercentage) : TrackSizeItem {
override fun toString(): String {
return "fit-content($value)"
}
}
public class LengthPercentage(private val v: CSSLengthOrPercentageValue) :
InflexibleBreadthItem, TrackBreadthItem, FixedBreadthItem {
override fun toString(): String {
return v.toString()
}
}
public object MinContent : InflexibleBreadthItem, TrackBreadthItem {
override fun toString(): String {
return "min-content"
}
}
public object MaxContent : InflexibleBreadthItem, TrackBreadthItem {
override fun toString(): String {
return "max-content"
}
}
public object Auto : InflexibleBreadthItem, TrackBreadthItem {
override fun toString(): String {
return "auto"
}
}
public class Flex(private val v: CSSSizeValue<CSSUnit.fr>) : TrackBreadthItem {
override fun toString(): String {
return v.toString()
}
}
}
public class GridArea internal constructor(private val breakpoint: Breakpoint?) {
private val rows: MutableList<List<String>> = mutableListOf()
public fun row(vararg cells: String) {
rows += listOf(*cells)
}
@Composable
internal fun StyleSheetBuilder.generateStyle(): String {
val classname = remember { "_${uuid4()}" }
withBreakpoint(breakpoint) {
className(classname) style {
property(
"grid-template-areas",
rows.joinToString(separator = " ") {
it.joinToString(separator = " ", prefix = "\"", postfix = "\"")
}
)
}
}
return classname
}
}
public class GridItemLayout {
private val areaSpecs: MutableList<GridItemArea> = mutableListOf()
private val placements: MutableList<PlacementSpec> = mutableListOf()
public fun area(breakpoint: Breakpoint? = null, name: String) {
areaSpecs += GridItemArea(breakpoint, name)
}
public fun area(name: String) {
areaSpecs += GridItemArea(null, name)
}
public fun area(breakpoint: Breakpoint, name: String) {
areaSpecs += GridItemArea(breakpoint, name)
}
public fun area(spec: GridItemArea.() -> Unit) {
areaSpecs += GridItemArea().apply(spec)
}
public fun area(breakpoint: Breakpoint, spec: GridItemArea.() -> Unit) {
areaSpecs += GridItemArea(breakpoint).apply(spec)
}
public fun placement(spec: PlacementSpec.() -> Unit) {
placements += PlacementSpec().apply(spec)
}
public fun placement(breakpoint: Breakpoint, spec: PlacementSpec.() -> Unit) {
placements += PlacementSpec(breakpoint).apply(spec)
}
@Composable
internal fun generate(): List<String> {
val classes: MutableList<String> = mutableListOf()
Style {
areaSpecs.forEach {
it.apply { classes += generateStyle() }
}
placements.forEach {
it.apply { classes += generateStyle() }
}
}
return classes
}
}
public class GridItemArea(private val breakpoint: Breakpoint? = null, private val name: String? = null) {
private var column: GridItemAreaSpec? = null
private var row: GridItemAreaSpec? = null
public fun row(f: GridItemAreaSpec.() -> Unit) {
row = GridItemAreaSpec().apply(f)
}
public fun row(start: Int, end: Int? = null) {
row = GridItemAreaSpec().apply {
start(start)
end?.let { end(it) }
}
}
public fun row(start: Int, end: GridLine? = null) {
row = GridItemAreaSpec().apply {
start(start)
end(end)
}
}
public fun row(start: GridLine, end: Int? = null) {
row = GridItemAreaSpec().apply {
start(start)
end?.let {
end(end)
}
}
}
public fun row(start: GridLine, end: GridLine?) {
row = GridItemAreaSpec().apply {
start(start)
end(end)
}
}
public fun column(f: GridItemAreaSpec.() -> Unit) {
column = GridItemAreaSpec().apply(f)
}
public fun column(start: Int, end: Int?) {
column = GridItemAreaSpec().apply {
start(start)
end?.let {
end(it)
}
}
}
public fun column(start: Int, end: GridLine?) {
column = GridItemAreaSpec().apply {
start(start)
end(end)
}
}
public fun column(start: GridLine, end: Int?) {
column = GridItemAreaSpec().apply {
start(start)
end?.let { end(it) }
}
}
public fun column(start: GridLine, end: GridLine) {
column = GridItemAreaSpec().apply {
start(start)
end(end)
}
}
@Composable
internal fun StyleSheetBuilder.generateStyle(): String {
val classname = remember { "_${uuid4()}" }
withBreakpoint(breakpoint) {
className(classname) style {
if (name != null) {
property("grid-area", name)
} else {
column?.let {
property("grid-column", it.toString())
}
row?.let {
property("grid-row", it.toString())
}
}
}
}
return classname
}
}
public class GridItemAreaSpec {
private var startPlacement: GridLine = GridLine.Auto
private var endPlacement: GridLine? = null
public fun start(ident: String) {
startPlacement = GridLine.CustomIdent(ident)
}
public fun start(line: Int, ident: String? = null) {
startPlacement = GridLine.Line(line, ident)
}
public fun end(ident: String) {
endPlacement = GridLine.CustomIdent(ident)
}
public fun end(line: Int, ident: String? = null) {
endPlacement = GridLine.Line(line, ident)
}
internal fun start(start: GridLine) {
startPlacement = start
}
internal fun end(end: GridLine?) {
end?.let {
endPlacement = it
}
}
public infix fun Int.to(end: Int) {
startPlacement = GridLine.Line(this)
endPlacement = GridLine.Line(end)
}
public infix fun Int.to(end: GridLine) {
startPlacement = GridLine.Line(this)
endPlacement = end
}
public infix fun GridLine.to(end: GridLine) {
startPlacement = this
endPlacement = end
}
public infix fun GridLine.to(end: Int) {
startPlacement = this
endPlacement = GridLine.Line(end)
}
public override fun toString(): String {
return startPlacement.toString() + (endPlacement?.let { " / $endPlacement" } ?: "")
}
}
public sealed class GridLine {
public object Auto : GridLine() {
override fun toString(): String {
return "auto"
}
}
internal data class CustomIdent(private val ident: String) : GridLine() {
override fun toString(): String {
return ident
}
}
internal data class Line(private val number: Int, private val ident: String? = null) : GridLine() {
override fun toString(): String {
return number.toString() + (ident ?: "")
}
}
public class Span private constructor(private val number: Int?, private val ident: String?) :
GridLine() {
public constructor(n: Int) : this(n, null)
public constructor(id: String) : this(null, id)
override fun toString(): String {
return "span " + (number?.toString() ?: ident)
}
}
}
public class PlacementSpec(private val breakpoint: Breakpoint? = null) {
public var block: PlacementType = PlacementType.Auto
public var inline: PlacementType? = null
public enum class PlacementType(private val value: String) {
Auto("auto"),
Start("start"),
End("end"),
Center("center"),
Stretch("stretch");
public override fun toString(): String {
return value
}
}
@Composable
internal fun StyleSheetBuilder.generateStyle(): String {
val classname = remember { "_${uuid4()}" }
withBreakpoint(breakpoint) {
className(classname) style {
property("place-self", block.toString() + (inline?.let { " $it" } ?: ""))
}
}
return classname
}
}
private fun StyleSheetBuilder.withBreakpoint(
breakpoint: Breakpoint?,
block: GenericStyleSheetBuilder<CSSStyleRuleBuilder>.() -> Unit
) {
val bp = breakpoints[breakpoint]
if (bp != null) {
media(mediaMinWidth(bp)) {
block()
}
} else {
block()
}
}

View File

@ -0,0 +1,30 @@
package bootstrap
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.I
import org.w3c.dom.HTMLElement
/**
* Bootstrap Icon shortcut when using with already embedded icons.
* Alternative use `bootstrap-compose-icons`.
*/
@Composable
public fun Icon(
iconName: String,
styling: (Styling.() -> Unit)? = null,
attrsBuilder: AttrBuilderContext<HTMLElement>? = null
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
I({
classes("bi", "bi-$iconName")
if (classes != null) {
classes(classes = classes)
}
attrsBuilder?.invoke(this)
})
}

View File

@ -0,0 +1,45 @@
package bootstrap
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.attributes.AutoComplete
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.attributes.autoComplete
import org.jetbrains.compose.web.attributes.builders.InputAttrsScope
import org.jetbrains.compose.web.attributes.placeholder
import org.jetbrains.compose.web.dom.Input
import org.jetbrains.compose.web.dom.Label
import org.jetbrains.compose.web.events.SyntheticInputEvent
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.Text
@Composable
public fun <T> Input(
label: String,
value: String,
type: InputType<T>,
placeholder: String? = null,
autocomplete: AutoComplete = AutoComplete.off,
labelClasses: String = "form-label",
inputClasses: String = "form-control",
attrs: (InputAttrsScope<T>.() -> Unit)? = null,
onInput: (SyntheticInputEvent<T, HTMLInputElement>) -> Unit
) {
Style
Label(forId = null, attrs = {
classes(labelClasses)
}) {
Text(label)
Input(type = type, attrs = {
autoComplete(autocomplete)
attrs?.invoke(this)
classes(inputClasses)
value(value)
if (placeholder != null) {
placeholder(placeholder)
}
onInput {
onInput(it)
}
})
}
}

View File

@ -0,0 +1,561 @@
package bootstrap
import androidx.compose.runtime.Composable
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.remember
import com.benasher44.uuid.uuid4
import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.attributes.builders.InputAttrsScope
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.dom.Text
import org.jetbrains.compose.web.events.SyntheticInputEvent
import org.w3c.dom.*
@Composable
public fun InputGroup(
inputId: String = remember { "_${uuid4()}" },
size: InputGroupSize = InputGroupSize.Default,
content: @Composable InputGroupContext.() -> Unit,
) {
Style
val scope = InputGroupContext(inputId)
Div(
attrs = {
classes(BSClasses.inputGroup)
when (size) {
InputGroupSize.Small -> classes(BSClasses.inputGroupSmall)
InputGroupSize.Large -> classes(BSClasses.inputGroupLarge)
else -> {
}
}
}
) {
scope.content()
}
}
public class InputGroupContext(private val inputId: String) {
private fun <K> InputAttrsScope<K>.buildInputAttrs(
disabled: Boolean = false,
autocomplete: AutoComplete = AutoComplete.off,
classes: List<String>?,
attrs: (InputAttrsScope<K>.() -> Unit)? = null,
onInput: (SyntheticInputEvent<K, HTMLInputElement>) -> Unit,
) {
classes(BSClasses.formControl)
if (classes != null) {
classes(classes)
}
id(inputId)
if (disabled) {
disabled()
}
autoComplete(autocomplete)
attrs?.invoke(this)
onInput { event ->
onInput(event)
}
}
@Composable
@NonRestartableComposable
public fun <K> Input(
type: InputType<K>,
autocomplete: AutoComplete = AutoComplete.off,
styling: (Styling.() -> Unit)? = null,
attrs: (InputAttrsScope<K>.() -> Unit)? = null,
onInput: (SyntheticInputEvent<K, HTMLInputElement>) -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Input(
type = type,
attrs = {
buildInputAttrs(false, autocomplete, classes, attrs, onInput)
}
)
}
@Composable
@NonRestartableComposable
public fun DateInput(
value: String,
autocomplete: AutoComplete = AutoComplete.off,
disabled: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrsBuilder: (InputAttrsScope<String>.() -> Unit)? = null,
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
org.jetbrains.compose.web.dom.DateInput(value) {
buildInputAttrs(disabled, autocomplete, classes, attrsBuilder, onInput)
}
}
@Composable
@NonRestartableComposable
public fun DateTimeLocalInput(
value: String,
autocomplete: AutoComplete = AutoComplete.off,
disabled: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrsBuilder: (InputAttrsScope<String>.() -> Unit)? = null,
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
org.jetbrains.compose.web.dom.DateTimeLocalInput(value) {
buildInputAttrs(disabled, autocomplete, classes, attrsBuilder, onInput)
}
}
@Composable
@NonRestartableComposable
public fun EmailInput(
value: String,
autocomplete: AutoComplete = AutoComplete.off,
disabled: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrsBuilder: (InputAttrsScope<String>.() -> Unit)? = null,
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
org.jetbrains.compose.web.dom.EmailInput(value) {
buildInputAttrs(disabled, autocomplete, classes, attrsBuilder, onInput)
}
}
@Composable
@NonRestartableComposable
public fun FileInput(
value: String,
autocomplete: AutoComplete = AutoComplete.off,
disabled: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrsBuilder: (InputAttrsScope<String>.() -> Unit)? = null,
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
org.jetbrains.compose.web.dom.FileInput(value) {
buildInputAttrs(disabled, autocomplete, classes, attrsBuilder, onInput)
}
}
@Composable
@NonRestartableComposable
public fun HiddenInput(
disabled: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrsBuilder: (InputAttrsScope<String>.() -> Unit)? = null,
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
org.jetbrains.compose.web.dom.HiddenInput {
buildInputAttrs(disabled, autocomplete = AutoComplete.off, classes, attrsBuilder, onInput)
}
}
@Composable
@NonRestartableComposable
public fun MonthInput(
value: String,
autocomplete: AutoComplete = AutoComplete.off,
disabled: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrsBuilder: (InputAttrsScope<String>.() -> Unit)? = null,
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
org.jetbrains.compose.web.dom.MonthInput(value) {
buildInputAttrs(disabled, autocomplete, classes, attrsBuilder, onInput)
}
}
@Composable
@NonRestartableComposable
public fun NumberInput(
value: Number?,
autocomplete: AutoComplete = AutoComplete.off,
min: Number? = null,
max: Number? = null,
disabled: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrsBuilder: (InputAttrsScope<Number?>.() -> Unit)? = null,
onInput: (SyntheticInputEvent<Number?, HTMLInputElement>) -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
NumberInput(value, min, max) {
buildInputAttrs(disabled, autocomplete, classes, attrsBuilder, onInput)
}
}
@Composable
@NonRestartableComposable
public fun PasswordInput(
value: String,
autocomplete: AutoComplete = AutoComplete.off,
disabled: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrsBuilder: (InputAttrsScope<String>.() -> Unit)? = null,
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
org.jetbrains.compose.web.dom.PasswordInput(value) {
buildInputAttrs(disabled, autocomplete, classes, attrsBuilder, onInput)
}
}
@Composable
@NonRestartableComposable
public fun SearchInput(
value: String,
autocomplete: AutoComplete = AutoComplete.off,
disabled: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrsBuilder: (InputAttrsScope<String>.() -> Unit)? = null,
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
org.jetbrains.compose.web.dom.SearchInput(value) {
buildInputAttrs(disabled, autocomplete, classes, attrsBuilder, onInput)
}
}
@Composable
@NonRestartableComposable
public fun TelInput(
value: String,
autocomplete: AutoComplete = AutoComplete.off,
disabled: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrsBuilder: (InputAttrsScope<String>.() -> Unit)? = null,
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
org.jetbrains.compose.web.dom.TelInput(value) {
buildInputAttrs(disabled, autocomplete, classes, attrsBuilder, onInput)
}
}
@Composable
public fun TextInput(
value: String,
placeholder: String? = null,
autocomplete: AutoComplete = AutoComplete.off,
disabled: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrs: (InputAttrsScope<String>.() -> Unit)? = null,
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
org.jetbrains.compose.web.dom.TextInput(value) {
buildInputAttrs(disabled, autocomplete, classes, attrs, onInput)
placeholder?.let {
placeholder(it)
}
}
}
@Composable
@NonRestartableComposable
public fun TextAreaInput(
value: String,
autocomplete: AutoComplete = AutoComplete.off,
disabled: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLTextAreaElement>? = null,
onInput: (SyntheticInputEvent<String, HTMLTextAreaElement>) -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
TextArea(attrs = {
classes(BSClasses.formControl)
if (classes != null) {
classes(classes = classes)
}
autoComplete(autocomplete)
id(inputId)
if (disabled) {
disabled()
}
attrs?.invoke(this)
onInput { event ->
onInput(event)
}
}, value = value)
}
@Composable
@NonRestartableComposable
public fun TimeInput(
value: String,
autocomplete: AutoComplete = AutoComplete.off,
disabled: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrs: (InputAttrsScope<String>.() -> Unit)? = null,
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
org.jetbrains.compose.web.dom.TimeInput(value) {
buildInputAttrs(disabled, autocomplete, classes, attrs, onInput)
}
}
@Composable
@NonRestartableComposable
public fun UrlInput(
value: String,
autocomplete: AutoComplete = AutoComplete.off,
disabled: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrs: (InputAttrsScope<String>.() -> Unit)? = null,
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
org.jetbrains.compose.web.dom.UrlInput(value) {
buildInputAttrs(disabled, autocomplete, classes, attrs, onInput)
}
}
@Composable
@NonRestartableComposable
public fun WeekInput(
value: String,
autocomplete: AutoComplete = AutoComplete.off,
disabled: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrs: (InputAttrsScope<String>.() -> Unit)? = null,
onInput: (SyntheticInputEvent<String, HTMLInputElement>) -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
org.jetbrains.compose.web.dom.WeekInput(value) {
buildInputAttrs(disabled, autocomplete, classes, attrs, onInput)
}
}
@Composable
@NonRestartableComposable
public fun SelectInput(
disabled: Boolean,
autocomplete: AutoComplete = AutoComplete.off,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLSelectElement>? = null,
onChange: (List<String>) -> Unit,
content: @Composable SelectContext.() -> Unit,
) {
Style
Select(
disabled = disabled,
id = inputId,
autocomplete = autocomplete,
styling = styling,
attrs = attrs,
onChange = onChange,
content = content
)
}
/**
* Implements the Input group add on as text in a Span element.
*/
@Composable
public fun TextAddOn(
text: String,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLSpanElement>? = null,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Span(attrs = {
classes(BSClasses.inputGroupText)
if (classes != null) {
classes(classes = classes)
}
attrs?.invoke(this)
}) {
Text(text)
}
}
/**
* Implements the Input group add on as text in a Label element. The label element's forId is set equal to the
* Input's id. An example of this usage is in the Custom select Bootstrap documentation
* at https://getbootstrap.com/docs/5.0/forms/input-group/#custom-select.
*/
@Composable
public fun LabelAddOn(
text: String,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLLabelElement>? = null,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Label(attrs = {
classes(BSClasses.inputGroupText)
if (classes != null) {
classes(classes = classes)
}
forId(inputId)
attrs?.invoke(this)
}) {
Text(text)
}
}
@Composable
public fun ButtonAddOn(
title: String,
color: Color = Color.Primary,
type: ButtonType = ButtonType.Submit,
size: ButtonSize = ButtonSize.Default,
disabled: Boolean = false,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLButtonElement>? = null,
action: () -> Unit,
) {
Style
Button(title, color, size, false, type, disabled, styling, attrs, action)
}
@Composable
public fun DropDownAddOn(
title: String,
color: Color = Color.Primary,
size: ButtonSize = ButtonSize.Default,
styling: (Styling.() -> Unit)? = null,
block: @Composable DropDownBuilder.() -> Unit,
) {
Style
DropDown(title, inputId, size, color, styling, block = block)
}
@Composable
public fun CheckboxAddOn(
checked: Boolean,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLDivElement>? = null,
onClick: (Boolean) -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Div(attrs = {
classes(BSClasses.inputGroupText)
if (classes != null) {
classes(classes = classes)
}
attrs?.invoke(this)
}) {
CheckboxInput(checked, attrs = {
onInput { event ->
onClick(event.value)
}
classes(BSClasses.formCheckInput, "mt-0")
})
}
}
@Composable
public fun RadioAddOn(
checked: Boolean,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLDivElement>? = null,
onClick: (Boolean) -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Div(attrs = {
classes(BSClasses.inputGroupText)
if (classes != null) {
classes(classes = classes)
}
attrs?.invoke(this)
}) {
RadioInput(checked, attrs = {
onInput { event ->
onClick(event.value)
}
classes(BSClasses.formCheckInput, "mt-0")
})
}
}
}
public enum class InputGroupSize {
Small,
Default,
Large
}

View File

@ -0,0 +1,182 @@
package bootstrap
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.attributes.AttrsScope
import org.jetbrains.compose.web.dom.*
import org.w3c.dom.*
@Composable
public fun ListGroup(
flush: Boolean = false,
listGroupDirection: ListGroupDirection = ListGroupDirection.Vertical,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLUListElement>? = null,
content: ContentBuilder<HTMLUListElement>? = null
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Ul(
attrs = {
ListGroupAttrs(flush, false, listGroupDirection, attrs)
if (classes != null) {
classes(classes = classes)
}
},
content = content
)
}
@Composable
public fun NumberedListGroup(
flush: Boolean = false,
listGroupDirection: ListGroupDirection = ListGroupDirection.Vertical,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLOListElement>? = null,
content: ContentBuilder<HTMLOListElement>? = null
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Ol(
attrs = {
ListGroupAttrs(flush, true, listGroupDirection, attrs)
if (classes != null) {
classes(classes = classes)
}
},
content = content
)
}
private fun <T : HTMLElement> AttrsScope<T>.ListGroupAttrs(
flush: Boolean = false,
numbered: Boolean = false,
listGroupDirection: ListGroupDirection = ListGroupDirection.Vertical,
attrs: AttrBuilderContext<T>? = null,
) {
classes(BSClasses.listGroup)
if (flush) {
classes(BSClasses.listGroupFlush)
}
if (numbered) {
classes(BSClasses.listGroupNumbered)
}
if (listGroupDirection is ListGroupDirection.Horizontal) {
classes(listGroupDirection.classname())
}
attrs?.invoke(this)
}
@Composable
public fun ListItem(
active: Boolean = false,
disabled: Boolean = false,
background: Color? = null,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLLIElement>? = null,
content: ContentBuilder<HTMLLIElement>? = null
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Li(attrs = {
ListItemAttrs(active, disabled, false, background, attrs)
if (classes != null) {
classes(classes = classes)
}
}, content = content)
}
@Composable
public fun AnchorListItem(
href: String? = null,
active: Boolean = false,
disabled: Boolean = false,
background: Color? = null,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLAnchorElement>? = null,
content: ContentBuilder<HTMLAnchorElement>? = null
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
A(
href = href,
attrs = {
ListItemAttrs(active, disabled, true, background, attrs)
if (classes != null) {
classes(classes = classes)
}
},
content = content
)
}
@Composable
public fun ButtonListItem(
active: Boolean = false,
disabled: Boolean = false,
background: Color? = null,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLButtonElement>? = null,
content: ContentBuilder<HTMLButtonElement>? = null
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Button(attrs = {
ListItemAttrs(active, disabled, true, background, attrs)
if (classes != null) {
classes(classes = classes)
}
}, content = content)
}
private fun <T : HTMLElement> AttrsScope<T>.ListItemAttrs(
active: Boolean = false,
disabled: Boolean = false,
actionable: Boolean = false,
background: Color? = null,
attrs: AttrBuilderContext<T>? = null,
) {
classes(BSClasses.listGroupItem)
if (active) {
classes(BSClasses.active)
attr("aria-current", "true")
}
if (disabled) {
classes(BSClasses.disabled)
attr("aria-disabled", "true")
}
if (actionable) {
classes(BSClasses.listGroupItemAction)
}
background?.let {
classes("${BSClasses.listGroupItem}-$it")
}
attrs?.invoke(this)
}
/**
* Specifies a ListGroup's direction, and optionally for the Horizontal variant a breakpoint.
*/
public sealed class ListGroupDirection {
public object Vertical : ListGroupDirection()
public data class Horizontal(val breakpoint: Breakpoint? = null) : ListGroupDirection() {
internal fun classname(): String {
return "${BSClasses.listGroupHorizontal}${breakpoint?.let { "-$it" } ?: ""}"
}
}
}

View File

@ -0,0 +1,84 @@
package bootstrap
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import com.benasher44.uuid.uuid4
import org.jetbrains.compose.web.dom.*
import org.w3c.dom.HTMLDivElement
@Composable
public fun Modal(
header: String,
size: Breakpoint? = null,
scrollable: Boolean = false,
id: String = uuid4().toString(),
styling: (Styling.() -> Unit)? = null,
onDismissRequest: () -> Unit,
footer: ContentBuilder<HTMLDivElement>? = null,
content: ContentBuilder<HTMLDivElement>,
) {
Style
needsJS
val classes = styling?.let {
Styling().apply(it).generate()
}
Div({
classes("modal")
id("_$id")
tabIndex(-1)
attr("aria-labelledby", "label$id")
attr("aria-hidden", "true")
attr("data-bs-backdrop", "static")
attr("data-bs-keyboard", "false")
}) {
DisposableEffect(true) {
val htmlDivElement: HTMLDivElement = scopeElement
val bsModal = Modal(htmlDivElement)
htmlDivElement.addEventListener("hidePrevented.bs.modal", callback = { _ ->
onDismissRequest()
})
bsModal.show()
onDispose {
bsModal.hide()
bsModal.dispose()
}
}
Div({
classes("modal-dialog")
if (size != null) {
classes("modal-$size")
}
if (scrollable) {
classes("modal-dialog-scrollable")
}
}) {
Div({
classes("modal-content")
if (classes != null) {
classes(classes = classes)
}
}) {
Div({ classes("modal-header") }) {
H5({
classes("modal-title")
id("label$id")
}) {
Text(header)
}
Button({
classes("btn-close")
// attr("data-bs-dismiss", "modal")
attr("aria-label", "Close")
onClick {
onDismissRequest()
}
})
}
Div({ classes("modal-body") }, content)
Div({ classes("modal-footer") }, footer)
}
}
}
}

View File

@ -0,0 +1,221 @@
package bootstrap
import androidx.compose.runtime.*
import com.benasher44.uuid.uuid4
import org.jetbrains.compose.web.dom.*
import org.w3c.dom.*
/**
* Bootstrap Navbar component. This version provides more flexibility than the alternative but is more complex
* to use. For a simpler implementation use the version that takes a brand and navItems as parameters.
*
* @param collapseBehavior Specifies the Navbar's Responsive behavior with use of the .navbar-expand class.
* @param fluid Specifies if the inner container is fluid (container-fluid) or not.
* @param placement Specifies the placement of the navbar
* @param containerBreakpoint Breakpoint for the inner container.
* @param colorScheme Valid values are Color.Light or Color.Dark to set the navbar-dark/light class.
* @param backgroundColor Background color to use, with the bg-* classes.
* @param content Navbar content, placed within the inner container.
*/
@Composable
public fun Navbar(
collapseBehavior: NavbarCollapseBehavior = NavbarCollapseBehavior.Always,
fluid: Boolean = false,
placement: NavbarPlacement = NavbarPlacement.Default,
containerBreakpoint: Breakpoint? = null,
colorScheme: Color = Color.Light,
backgroundColor: Color = Color.Primary,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLElement>? = null,
content: ContentBuilder<HTMLDivElement>? = null,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Nav(attrs = {
classes(
BSClasses.navbar,
"navbar-$colorScheme",
"bg-$backgroundColor"
)
when (placement) {
NavbarPlacement.Default -> {
}
else -> {
classes(placement.toString())
}
}
if (classes != null) {
classes(classes = classes)
}
when (collapseBehavior) {
is NavbarCollapseBehavior.Never -> classes("navbar-expand")
is NavbarCollapseBehavior.AtBreakpoint -> classes("navbar-expand-${collapseBehavior.breakpoint}")
is NavbarCollapseBehavior.Always -> {
} // No class needed for "Always" behavior
}
attr("role", "navigation")
attrs?.invoke(this)
}) {
Container(fluid, containerBreakpoint, content = content)
}
}
/**
* Bootstrap Navbar component that can optionally enable a Toggler and Brand.
*
* @param collapseBehavior Specifies the Navbar's Responsive behavior with use of the .navbar-expand class.
* @param fluid Specifies if the inner container is fluid (container-fluid) or not.
* @param stickyTop if true, shows the navbar at top
* @param containerBreakpoint Breakpoint for the inner container.
* @param colorScheme Valid values are Color.Light or Color.Dark to set the navbar-dark/light class.
* @param backgroundColor Background color to use, with the bg-* classes.
* @param toggler Whether a toggler button should be provided when falling below the breakpoint.
* @param togglerPosition Specifies if the toggler should be placed on the left or right side of the Navbar.
* @param togglerAttrs Additional attributes to set on the toggler button
* @param togglerTargetId Optional id of the toggler, using a random UUID by default
* @param attrs Additional attributes to set on the Navbar
* @param navAttrs Additional attributes to set on the navItems section of the Navbar
* @param brand Composable implementing the brand elements
* @param navItems navigation items, normally comprised of NavbarLink and NavbarDropDown menu items.
*/
@Composable
public fun Navbar(
collapseBehavior: NavbarCollapseBehavior = NavbarCollapseBehavior.Always,
fluid: Boolean = false,
placement: NavbarPlacement = NavbarPlacement.Default,
containerBreakpoint: Breakpoint? = null,
colorScheme: Color = Color.Light,
backgroundColor: Color = Color.Primary,
toggler: Boolean = true,
togglerPosition: TogglerPosition = TogglerPosition.Right,
togglerTargetId: String = remember { "toggler${uuid4()}" },
togglerAttrs: AttrBuilderContext<HTMLButtonElement>? = null,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLElement>? = null,
navAttrs: AttrBuilderContext<HTMLDivElement>? = null,
additionalNavContent: ContentBuilder<HTMLDivElement>? = null,
brand: ContentBuilder<HTMLDivElement>,
navItems: ContentBuilder<HTMLDivElement>,
) {
Navbar(
collapseBehavior,
fluid,
placement,
containerBreakpoint,
colorScheme,
backgroundColor,
styling,
attrs
) {
if (togglerPosition == TogglerPosition.Right) {
brand()
}
if (toggler) {
Toggler(
target = togglerTargetId,
controls = togglerTargetId,
styling = styling,
attrs = togglerAttrs
)
NavbarCollapse(togglerTargetId) {
NavbarNav(attrs = { navAttrs?.invoke(this) }) {
navItems()
}
if (additionalNavContent != null) {
additionalNavContent()
}
}
} else {
NavbarNav(attrs = { navAttrs?.invoke(this) }) {
navItems()
}
if (additionalNavContent != null) {
additionalNavContent()
}
}
if (togglerPosition == TogglerPosition.Left) {
brand()
}
}
}
/**
* Bootstrap navbar-collapse component to be used with the NavbarToggler.
* @param id The element id. This value should also be used as the target parameter with a NavbarToggler.
*/
@Composable
public fun NavbarCollapse(
id: String,
attrs: AttrBuilderContext<HTMLDivElement>? = null,
content: ContentBuilder<HTMLDivElement>? = null,
) {
Style
Div(attrs = {
classes(BSClasses.collapse, BSClasses.navbarCollapse)
id(id)
attrs?.invoke(this)
}, content = content)
}
/**
* Bootstrap navbar-nav component to be used within a Navbar.
*/
@Composable
public fun NavbarNav(
attrs: AttrBuilderContext<HTMLDivElement>? = null,
links: ContentBuilder<HTMLDivElement>? = null,
) {
Style
Div(attrs = {
classes(BSClasses.navbarNav)
attrs?.invoke(this)
}, content = links)
}
/**
* Bootstrap nav-link component.
*/
@Composable
public fun NavbarLink(
active: Boolean,
attrs: AttrBuilderContext<HTMLAnchorElement>? = null,
disabled: Boolean = false,
link: String? = null,
content: ContentBuilder<HTMLAnchorElement>? = null,
) {
Style
A(attrs = {
classes(BSClasses.navLink)
if (active) {
classes(BSClasses.active)
attr("aria-current", "page")
}
if (disabled) {
classes(BSClasses.disabled)
}
attrs?.invoke(this)
}, href = link, content = content)
}
public enum class TogglerPosition {
Left, Right
}
public sealed class NavbarCollapseBehavior {
public object Never : NavbarCollapseBehavior()
public object Always : NavbarCollapseBehavior()
public data class AtBreakpoint(val breakpoint: Breakpoint) : NavbarCollapseBehavior()
}
public enum class NavbarPlacement(private val prefix: String) {
Default(""), FixedTop("fixed-top"), FixedBottom("fixed-bottom"), StickyTop("sticky-top");
override fun toString(): String = prefix
}

View File

@ -0,0 +1,137 @@
package bootstrap
import androidx.compose.runtime.*
import org.jetbrains.compose.web.attributes.ButtonType
import org.jetbrains.compose.web.attributes.type
import org.jetbrains.compose.web.dom.Button
import org.jetbrains.compose.web.dom.ContentBuilder
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLDivElement
/**
* State for [Offcanvas] composable component. Provides functions to call
* into Bootstrap's Javascript to show/hide the Offcanvas element.
*
* @param confirmStateChange Optional callback invoked to signal a state change.
*/
public class OffcanvasState(public val confirmStateChange: (Boolean) -> Unit = {}) {
internal var bsOffcanvas: Offcanvas? = null
private var _visible by mutableStateOf(false)
/**
* The current visibility state.
*/
public val visible: Boolean
get() {
return _visible
}
/**
* Shows the Offcanvas component.
*/
public fun show() {
_visible = true
confirmStateChange(_visible)
bsOffcanvas?.apply { show() }
}
/**
* Hides the Offcanvas component.
*/
public fun hide() {
_visible = false
confirmStateChange(_visible)
bsOffcanvas?.apply { hide() }
}
}
/**
* Creates an Offcanvas component.
*
* @param placement The location for the Offcanvas component.
* @param offcanvasState Control for showing/hiding the Offcanvas component.
* @param headerContent Composable to render content for the header. A close button will always
* be provided on the right.
* @param showHeaderCloseButton If true the default Bootstrap close button will be shown on the right side of the
* header. If false, the user should provide their own control for closing the Offcanvas component.
* @param bodyContent Composable to render content for the body of the Offcanvas component.
*/
@Composable
public fun Offcanvas(
placement: OffcanvasPlacement,
breakpoint: Breakpoint? = null,
offcanvasState: OffcanvasState = remember { OffcanvasState() },
headerContent: ContentBuilder<HTMLDivElement>? = null,
showHeaderCloseButton: Boolean = true,
bodyContent: ContentBuilder<HTMLDivElement>? = null
) {
Style
needsJS
Div(attrs = {
if (breakpoint == null) {
classes("offcanvas")
} else {
classes("offcanvas-$breakpoint")
}
classes(placement.toString())
tabIndex(-1)
}) {
DisposableEffect(true) {
val htmlDivElement = scopeElement
offcanvasState.bsOffcanvas = Offcanvas(htmlDivElement)
// synchronize state with the offcanvas visibility
htmlDivElement.addEventListener("hidden.bs.offcanvas", callback = {
offcanvasState.hide()
})
htmlDivElement.addEventListener("shown.bs.offcanvas", callback = {
offcanvasState.show()
})
// Set initial state
offcanvasState.bsOffcanvas?.apply {
if (offcanvasState.visible) {
show()
} else {
hide()
}
}
onDispose {
offcanvasState.bsOffcanvas?.hide()
offcanvasState.bsOffcanvas = null
}
}
Div(attrs = {
classes("offcanvas-header")
}) {
headerContent?.invoke(this)
if (showHeaderCloseButton) {
Button(attrs = {
type(ButtonType.Button)
classes("btn-close", "text-reset")
attr("aria-label", "Close")
onClick {
offcanvasState.bsOffcanvas?.hide()
}
})
}
}
Div(attrs = { classes("offcanvas-body") }, content = bodyContent)
}
}
/**
* Used to specify the location of the Offcanvas component.
*/
public enum class OffcanvasPlacement(private val value: String) {
START("start"),
END("end"),
TOP("top"),
BOTTOM("bottom");
public override fun toString(): String {
return "offcanvas-$value"
}
}

View File

@ -0,0 +1,67 @@
package bootstrap
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.dom.*
import org.w3c.dom.HTMLLIElement
import org.w3c.dom.HTMLUListElement
@Composable
public fun Pagination(
size: PaginationSize = PaginationSize.Normal,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLUListElement>? = null,
content: @Composable PaginationScope.() -> Unit
) {
Style
val style = styling?.let {
Styling().apply(it).generate()
}
Ul(attrs = {
classes("pagination")
classes(size.toString())
if (style != null) {
classes(classes = style)
}
attrs?.invoke(this)
}) {
PaginationScope().content()
}
}
public enum class PaginationSize(private val classes: String) {
Normal(""), Small("pagination-sm"), Large("pagination-lg");
override fun toString(): String = classes
}
public class PaginationScope {
@Composable
public fun PageItem(active: Boolean = false, disabled: Boolean = false, content: ContentBuilder<HTMLLIElement>) {
Style
Li(attrs = {
classes("page-item")
if (active) {
classes(BSClasses.active)
}
if (disabled) {
classes(BSClasses.disabled)
}
}) {
content()
}
}
@Composable
public fun PageLink(title: String, onClick: () -> Unit) {
Style
A(attrs = {
classes("page-link")
onClick {
onClick()
}
}) {
Text(title)
}
}
}

View File

@ -0,0 +1 @@
This implementation is based on https://github.com/hfhbd/bootstrap-compose, which is distributed under Apache 2.0 license.

View File

@ -0,0 +1,69 @@
package bootstrap
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.benasher44.uuid.uuid4
import org.jetbrains.compose.web.attributes.InputType
import org.jetbrains.compose.web.attributes.disabled
import org.jetbrains.compose.web.attributes.forId
import org.jetbrains.compose.web.attributes.name
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Input
import org.jetbrains.compose.web.dom.Label
import org.jetbrains.compose.web.dom.Text
@Composable
public fun RadioGroup(content: @Composable RadioGroupScope.() -> Unit) {
val groupName = remember { "_${uuid4()}" }
val context = RadioGroupScope(groupName)
context.content()
}
public class RadioGroupScope(private val name: String) {
@Composable
public fun Radio(
label: String,
checked: Boolean = false,
disabled: Boolean = false,
inline: Boolean = false,
styling: (Styling.() -> Unit)? = null,
onClick: (Boolean) -> Unit,
) {
val id = remember { "_${uuid4()}" }
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Div(attrs = {
classes(BSClasses.formCheck)
if (inline) {
classes(BSClasses.formCheckInline)
}
if (classes != null) {
classes(classes = classes)
}
}) {
Input(attrs = {
classes(BSClasses.formCheckInput)
id(id)
checked(checked)
onInput { event ->
onClick(event.value)
}
if (disabled) {
disabled()
}
name(name)
}, type = InputType.Radio)
Label(attrs = {
classes(BSClasses.formCheckLabel)
forId(id)
}) {
Text(label)
}
}
}
}

View File

@ -0,0 +1,43 @@
package bootstrap
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.benasher44.uuid.uuid4
import org.jetbrains.compose.web.attributes.disabled
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.RangeInput
import org.jetbrains.compose.web.events.SyntheticInputEvent
import org.w3c.dom.HTMLInputElement
@Composable
public fun Range(
value: Number,
min: Number? = null,
max: Number? = null,
step: Number = 1,
disabled: Boolean = false,
id: String = remember { "_${uuid4()}" },
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLInputElement>? = null,
onInput: (SyntheticInputEvent<Number?, HTMLInputElement>) -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
RangeInput(value, min, max, step) {
if (classes != null) {
classes(classes = classes)
}
onInput {
onInput(it)
}
if (disabled) {
disabled()
}
id(id)
classes(BSClasses.formRange)
attrs?.invoke(this)
}
}

View File

@ -0,0 +1,94 @@
package bootstrap
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.ContentBuilder
import org.jetbrains.compose.web.dom.Div
import org.w3c.dom.HTMLDivElement
@Composable
public fun Row(
styling: (@Composable RowStyling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLDivElement>? = null,
content: ContentBuilder<HTMLDivElement>
) {
Style
val classes = styling?.let {
RowStyling(it).generate()
}
Div(attrs = {
classes("row")
if (classes != null) {
classes(classes = classes)
}
attrs?.invoke(this)
}, content = content)
}
@Composable
public fun RowStyling(styling: @Composable RowStyling.() -> Unit): RowStyling = RowStyling().apply { styling() }
public class RowStyling : Styling() {
public val Gutters: Gutters = Gutters()
@Composable
override fun generate(): List<String> {
return super.generate() + Gutters.generate()
}
}
public class Gutters {
public operator fun invoke(f: Gutters.() -> Unit) {
this.f()
}
@Composable
internal fun generate(): List<String> {
val classes: MutableList<String> = mutableListOf()
_spec?.let {
classes += it.generateClassStrings()
}
return classes
}
public enum class GutterSize(private val value: String) {
None("0"),
ExtraSmall("1"),
Small("2"),
Medium("3"),
Large("4"),
ExtraLarge("5");
override fun toString(): String {
return value
}
}
public enum class Direction(private val value: String) {
Horizontal("x"),
Vertical("y"),
HorizontalAndVertical("");
override fun toString(): String {
return value
}
}
public class GutterSpec(private val direction: Direction) {
public var size: GutterSize = GutterSize.Small
public var breakpoint: Breakpoint? = null
internal fun generateClassStrings(): String {
return "g$direction-" + (breakpoint?.let { "$it-" } ?: "") + size.toString()
}
}
private var _spec: GutterSpec? = null
public operator fun Direction.invoke(spec: GutterSpec.() -> Unit) {
_spec = GutterSpec(this).apply(spec)
}
}

View File

@ -0,0 +1,105 @@
package bootstrap
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import com.benasher44.uuid.uuid4
import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.ContentBuilder
import org.jetbrains.compose.web.dom.Select
import org.w3c.dom.HTMLOptionElement
import org.w3c.dom.HTMLSelectElement
@Composable
public fun Select(
multiple: Boolean = false,
size: SelectSize = SelectSize.Default,
rows: Int? = null,
disabled: Boolean = false,
autocomplete: AutoComplete = AutoComplete.off,
id: String = remember { "_${uuid4()}" },
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLSelectElement>? = null,
onChange: (List<String>) -> Unit,
content: @Composable SelectContext.() -> Unit,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Select(attrs = {
id(id)
classes(BSClasses.formSelect)
if (classes != null) {
classes(classes = classes)
}
if (multiple) {
multiple()
}
when (size) {
SelectSize.Small -> classes(BSClasses.formSelectSmall)
SelectSize.Large -> classes(BSClasses.formSelectLarge)
else -> {
}
}
rows?.let {
size(it)
}
if (disabled) {
disabled()
}
autoComplete(autocomplete)
attrs?.invoke(this)
onChange { event ->
val selectElement = event.nativeEvent.target as HTMLSelectElement
val options = selectElement.selectedOptions
val results: MutableList<String> = mutableListOf()
for (i in 0..options.length) {
options.item(i)?.let {
results += (it as HTMLOptionElement).value
}
}
onChange(results)
}
}) {
val scope = SelectContext()
scope.content()
}
}
public class SelectContext {
@Composable
public fun Option(
value: String,
selected: Boolean? = null,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLOptionElement>? = null,
content: ContentBuilder<HTMLOptionElement>? = null,
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
org.jetbrains.compose.web.dom.Option(
value = value,
attrs = {
if (selected == true) {
selected()
}
if (classes != null) {
classes(classes = classes)
}
attrs?.invoke(this)
},
content = content
)
}
}
public enum class SelectSize {
Small,
Default,
Large
}

View File

@ -0,0 +1,557 @@
package bootstrap
import androidx.compose.runtime.Composable
@DslMarker
public annotation class StylingMarker
/**
* Primary entrypoint for the Styling DSL. Component authors may create a subclass to add additional styling
* capabilities to their component, including the dynamic generation of classes by using Compose-web style sheet
* builders.
*/
@StylingMarker
public open class Styling {
private val generators: MutableList<@Composable () -> List<String>> = mutableListOf()
public val Margins: SpacingSpecs = SpacingSpecs("m")
public val Padding: SpacingSpecs = SpacingSpecs("p")
public val Borders: BorderSpec = BorderSpec()
public val Background: Background = Background()
public val Text: Text = Text()
public val Layout: Layout = Layout()
/**
* This function will generate a list of css class names that should be applied to a component in order
* to implement the styling features specified by the dsl. Subclasses should invoke this method and add
* any additional class names to the returned list. This method is Composable so additional style tags can
* be composed if new css class definitions are required.
*
* @return An array of css class names that are to be applied to the target component.
*/
@Composable
public open fun generate(): List<String> {
val classes: MutableList<String> = mutableListOf()
for (gen in generators) {
classes += gen()
}
classes += Margins.generateClassStrings() +
Padding.generateClassStrings() +
Borders.generateClassStrings() +
Background.generateClassStrings() +
Text.generateClassStrings() +
Layout.generateClassStrings()
return classes
}
/**
* Register a styling generator. The provided function is a Composable that may create new styles and classes,
* and returns an array of classnames that are to be applied to the target component.
*/
public fun registerGenerator(block: @Composable () -> List<String>) {
generators += block
}
}
@Composable
public fun Styling(styling: @Composable Styling.() -> Unit): Styling = Styling().apply { styling() }
@StylingMarker
public class SpacingSpecs(private val property: String) {
public operator fun invoke(f: SpacingSpecs.() -> Unit) {
this.f()
}
public enum class Sides(private val value: String) {
All(""),
Top("t"),
Bottom("b"),
Start("s"),
End("e"),
Vertical("y"),
Horizontal("x");
override fun toString(): String {
return value
}
}
public enum class SpacingSize(private val value: String) {
None("0"),
ExtraSmall("1"),
Small("2"),
Medium("3"),
Large("4"),
ExtraLarge("5"),
Auto("auto");
override fun toString(): String {
return value
}
}
public class SideSpec(private val sides: List<Sides>) {
public var size: SpacingSize = SpacingSize.Small
public var breakpoint: Breakpoint? = null
internal fun generateClassStrings(property: String): List<String> {
return sides.map { side ->
"$property$side-" + (breakpoint?.let { "$it-" } ?: "") + "$size"
}
}
}
private val _specs: MutableList<SideSpec> = mutableListOf()
public operator fun Sides.plus(side: Sides): List<Sides> {
return listOf(this, side)
}
public operator fun Sides.invoke(spec: SideSpec.() -> Unit) {
_specs += SideSpec(listOf(this)).apply(spec)
}
public operator fun List<Sides>.invoke(spec: SideSpec.() -> Unit) {
_specs += SideSpec(this).apply(spec)
}
internal fun generateClassStrings(): List<String> {
return _specs.flatMap { spec ->
spec.generateClassStrings(property)
}
}
public val All: Sides = Sides.All
public val Top: Sides = Sides.Top
public val Bottom: Sides = Sides.Bottom
public val Start: Sides = Sides.Start
public val End: Sides = Sides.End
public val Vertical: Sides = Sides.Vertical
public val Horizontal: Sides = Sides.Horizontal
}
@StylingMarker
public class BorderSpec {
public operator fun invoke(f: BorderSpec.() -> Unit) {
this.f()
}
public enum class Sides(private val value: String) {
All("border"),
Top("border-top"),
End("border-end"),
Bottom("border-bottom"),
Start("border-start");
override fun toString(): String {
return value
}
}
public enum class BorderWidth(private val value: String) {
Thinner("border-1"),
Thin("border-2"),
Medium("border-3"),
Thick("border-4"),
Thicker("border-5");
override fun toString(): String {
return value
}
}
public val All: Sides = Sides.All
public val Top: Sides = Sides.Top
public val End: Sides = Sides.End
public val Bottom: Sides = Sides.Bottom
public val Start: Sides = Sides.Start
public operator fun Sides.plus(side: Sides): List<Sides> {
return listOf(this, side)
}
public class SideSpec(private val sides: List<Sides>) {
public var width: BorderWidth? = null
public var color: Color? = null
internal fun generateClassString(): List<String> {
val classList: MutableList<String> = mutableListOf()
sides.map {
classList += it.toString()
}
return classList
}
}
public enum class BorderRadius(private val value: String) {
All("rounded"),
Top("rounded-top"),
End("rounded-end"),
Bottom("rounded-bottom"),
Start("rounded-start"),
Circle("rounded-circle"),
Pill("rounded-pill");
override fun toString(): String {
return value
}
}
public enum class RadiusSize(private val value: String) {
Small("rounded-1"),
Medium("rounded-2"),
Large("rounded-3");
override fun toString(): String {
return value
}
}
private var sideSpecs: SideSpec? = null
public operator fun Sides.invoke(spec: SideSpec.() -> Unit) {
sideSpecs = SideSpec(listOf(this)).apply(spec)
}
public operator fun List<Sides>.invoke(spec: SideSpec.() -> Unit) {
sideSpecs = SideSpec(this).apply(spec)
}
private var radiusType: BorderRadius? = null
private var radiusSize: RadiusSize? = null
public fun radius(type: BorderRadius, size: RadiusSize? = null) {
radiusType = type
radiusSize = size
}
internal fun generateClassStrings(): List<String> {
val classList: MutableList<String> = mutableListOf()
sideSpecs?.let { sideSpecs ->
classList += sideSpecs.generateClassString()
sideSpecs.color?.let { color ->
classList += "border-$color"
}
sideSpecs.width?.let { width ->
classList += width.toString()
}
}
classList += listOfNotNull(radiusType, radiusSize).map {
it.toString()
}
return classList
}
}
@StylingMarker
public class Background {
public var color: Color? = null
public var gradient: Boolean = false
public operator fun invoke(f: Background.() -> Unit) {
this.f()
}
internal fun generateClassStrings(): List<String> {
val classes: MutableList<String> = mutableListOf()
color?.let { color ->
classes += color.background()
if (gradient) {
classes += "bg-gradient"
}
}
return classes
}
}
@StylingMarker
public class Text {
public var color: Color? = null
private val alignments: MutableList<AlignSpec> = mutableListOf()
public var wrap: Wraps? = null
public var wordBreak: Boolean = false
public var transform: Transform? = null
public var size: Int? = null
public var weight: Weight? = null
public var style: Style? = null
public var lineHeight: LineHeight? = null
public var monospace: Boolean = false
public var reset: Boolean = false
public var muted: Boolean = false
public var decoration: Decoration? = null
public operator fun invoke(f: Text.() -> Unit) {
this.f()
}
public enum class Alignment(private val value: String) {
Start("start"),
Center("center"),
End("end");
override fun toString(): String {
return value
}
}
public enum class Wraps(private val value: String) {
Wrap("text-wrap"),
NoWrap("text-nowrap"),
Truncate("text-truncate");
override fun toString(): String {
return value
}
}
public enum class Transform(private val value: String) {
Lowercase("text-lowercase"),
Uppercase("text-uppercase"),
Capitalized("text-capitalize");
override fun toString(): String {
return value
}
}
public enum class Weight(private val value: String) {
Bold("fw-bold"),
Bolder("fw-bolder"),
Normal("fw-normal"),
Light("fw-light"),
Lighter("fw-lighter");
override fun toString(): String {
return value
}
}
public enum class Style(private val value: String) {
Italic("fst-italic"),
Normal("fst-normal");
override fun toString(): String {
return value
}
}
public enum class LineHeight(private val value: String) {
Smallest("lh-1"),
Small("lh-sm"),
Base("lh-base"),
Large("lh-large");
override fun toString(): String {
return value
}
}
public enum class Decoration(private val value: String) {
Underline("text-decoration-underline"),
LineThrough("text-decoration-line-through"),
None("text-decoration-none");
override fun toString(): String {
return value
}
}
public data class AlignSpec(private val alignment: Alignment, private var breakpoint: Breakpoint? = null) {
internal fun className(): String {
return breakpoint?.let {
"text-$it-$alignment"
} ?: "text-$alignment"
}
}
public fun align(alignment: Alignment, breakpoint: Breakpoint? = null) {
alignments += AlignSpec(alignment, breakpoint)
}
internal fun generateClassStrings(): List<String> {
val classes: MutableList<String> = mutableListOf()
color?.let { color ->
classes += color.text()
}
alignments.forEach { spec ->
classes += spec.className()
}
if (wordBreak) {
classes += "text-break"
}
size?.let {
classes += "fs-${it.coerceIn(1, 6)}"
}
if (monospace) {
classes += "font-monospace"
}
if (reset) {
classes += "text-reset"
}
if (muted) {
classes += "text-muted"
}
decoration?.let {
classes += it.toString()
}
classes += listOfNotNull(wrap, transform, weight, style, lineHeight).map {
it.toString()
}
return classes
}
}
@StylingMarker
public class Layout {
private val displaySpecs: MutableList<DisplaySpec> = mutableListOf()
private val floatSpecs: MutableList<FloatSpec> = mutableListOf()
public var overflow: Overflow? = null
public var width: Width? = null
public var height: Height? = null
public var verticalAlignment: VerticalAlignment? = null
public var horizontalAlignment: HorizontalAlignment? = null
public var visible: Boolean? = null
public operator fun invoke(f: Layout.() -> Unit) {
this.f()
}
public enum class Display(private val value: String) {
None("none"),
Inline("inline"),
InlineBlock("inline-block"),
Block("block"),
Grid("grid"),
Flex("flex"),
InlineFlex("inline-flex");
override fun toString(): String {
return value
}
}
public enum class Float(private val value: String) {
Start("start"),
End("end"),
None("none");
override fun toString(): String {
return value
}
}
public enum class Overflow(private val value: String) {
Auto("overflow-auto"),
Hidden("overflow-hidden"),
Visible("overflow-visible"),
Scroll("overflow-scroll");
override fun toString(): String {
return value
}
}
public enum class Width(private val value: String) {
Quarter("w-25"),
Half("w-50"),
ThreeQuarters("w-75"),
Full("w-100"),
Auto("w-auto"),
View("vw-100");
override fun toString(): String {
return value
}
}
public enum class Height(private val value: String) {
Quarter("h-25"),
Half("h-50"),
ThreeQuarters("h-75"),
Full("h-100"),
Auto("h-auto"),
View("vh-100");
override fun toString(): String {
return value
}
}
public enum class HorizontalAlignment(private val classInfix: String) {
Start("start"),
Center("center"),
End("end"),
Around("around"),
Between("between"),
Evenly("evenly");
override fun toString(): String = "justify-content-$classInfix"
}
public enum class VerticalAlignment(private val value: String) {
Baseline("align-baseline"),
Top("align-top"),
Middle("align-middle"),
Bottom("align-bottom"),
TextTop("align-text-top"),
TextBottom("align-text-bottom");
override fun toString(): String = value
}
private data class DisplaySpec(val display: Display, val breakpoint: Breakpoint?)
private data class FloatSpec(val float: Float, val breakpoint: Breakpoint?)
public fun display(display: Display, breakpoint: Breakpoint? = null) {
displaySpecs += DisplaySpec(display, breakpoint)
}
public fun float(float: Float, breakpoint: Breakpoint? = null) {
floatSpecs += FloatSpec(float, breakpoint)
}
internal fun generateClassStrings(): List<String> {
val classes: MutableList<String> = mutableListOf()
displaySpecs.forEach { displaySpec ->
classes += "d-" + (displaySpec.breakpoint?.let { "$it-" } ?: "") + "${displaySpec.display}"
}
floatSpecs.forEach { floatSpec ->
classes += "float-" + (floatSpec.breakpoint?.let { "$it-" } ?: "") + "${floatSpec.float}"
}
visible?.let { visible ->
classes += if (visible) {
"visible"
} else {
"invisible"
}
}
classes += listOfNotNull(overflow, width, height, verticalAlignment, horizontalAlignment).map {
it.toString()
}
return classes
}
}

View File

@ -0,0 +1,449 @@
package bootstrap
import androidx.compose.runtime.*
import org.jetbrains.compose.web.attributes.Scope
import org.jetbrains.compose.web.attributes.scope
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.*
import org.w3c.dom.HTMLDivElement
import org.w3c.dom.HTMLTableCaptionElement
import org.w3c.dom.HTMLTableCellElement
import org.w3c.dom.HTMLTableElement
import kotlin.collections.List
import kotlin.collections.all
import kotlin.collections.chunked
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.collections.forEach
import kotlin.collections.forEachIndexed
import kotlin.collections.lastIndex
import kotlin.collections.map
import kotlin.collections.mapIndexed
import kotlin.collections.mutableListOf
import kotlin.collections.mutableMapOf
import kotlin.collections.set
import kotlin.math.max
import kotlin.math.min
public typealias CurrentPage<T> = Table.Pagination.Page<T>
public typealias PageControl<T> = @Composable ElementScope<HTMLDivElement>.(
List<Table.Pagination.Page<T>>,
CurrentPage<T>,
(Int) -> Unit
) -> Unit
public object Table {
public data class Column(
val title: String,
val scope: Scope?,
val header: Header,
val cell: Cell,
val footer: Footer?
)
public data class Cell internal constructor(
public val color: Color? = null,
val scope: Scope?,
val verticalAlignment: Layout.VerticalAlignment?,
val content: ContentBuilder<HTMLTableCellElement>
)
public data class Footer internal constructor(
public val color: Color? = null,
val content: @Composable ElementScope<HTMLTableCellElement>.(List<Cell>) -> Unit
)
public data class Header(
val attrs: AttrBuilderContext<HTMLTableCellElement>? = null,
var content: ContentBuilder<HTMLTableCellElement>? = null
) {
public constructor(
color: Color,
) : this(attrs = { classes("table-$color") })
public constructor(
color: Color,
content: ContentBuilder<HTMLTableCellElement>? = null
) : this(attrs = { classes("table-$color") }, content)
}
public data class Row(val cells: List<Cell>, public val key: Any?, val color: Color? = null)
public class Builder internal constructor() {
private val values = mutableListOf<Column>()
public var rowColor: Color? = null
internal fun build(): Pair<List<Column>, Color?> = values to rowColor
public fun column(
title: String,
scope: Scope? = null,
header: Header? = null,
footer: Footer? = null,
cellColor: Color? = null,
verticalAlignment: Layout.VerticalAlignment? = null,
cell: ContentBuilder<HTMLTableCellElement>
) {
val titledHeader = when {
header != null && header.content == null -> {
header.content = { Text(title) }
header
}
header == null -> {
Header(attrs = null) { Text(title) }
}
else -> header
}
values.add(
Column(
title = title,
scope = scope,
header = titledHeader,
footer = footer,
cell = Cell(cellColor, scope, verticalAlignment, cell)
)
)
}
}
public class FixedHeaderProperty(public val style: StyleScope.() -> Unit) {
public constructor(topSize: CSSLengthOrPercentageValue, zIndex: ZIndex) : this({
top(topSize)
property("z-index", zIndex.unsafeCast<String>())
})
}
public interface Pagination<T> {
public enum class Position {
Top, Bottom
}
public val position: Position?
public val pages: State<List<Page<T>>>
public val numberOfButtons: Int
public val entriesPerPageLimit: State<Int>?
public val actionNavigateBack: ((CurrentPage<T>, Page<T>) -> Unit)?
public val actionNavigateForward: ((CurrentPage<T>, Page<T>) -> Unit)?
public val startPageIndex: Int
public data class Page<T>(val index: Int, val items: List<T>, val numberOfPages: Int)
public val control: PageControl<T>
public fun defaultControl(): PageControl<T> = { pages, currentPage, goTo ->
Column {
Pagination(size = PaginationSize.Small) {
PageItem(disabled = currentPage.index == 0) {
PageLink("<") {
val previousIndex = currentPage.index - 1
actionNavigateBack?.invoke(currentPage, pages[previousIndex])
goTo(previousIndex)
}
}
val buttons = tableCalcButtons(
index = currentPage.index,
pages = pages.size,
numberOfButtons = numberOfButtons
)
for (index in buttons) {
if (index == currentPage.index) {
PageItem(active = true, disabled = false) {
PageLink("${index + 1}") { }
}
} else {
PageItem {
PageLink("${index + 1}") {
if (index < currentPage.index) {
actionNavigateBack?.invoke(currentPage, pages[index])
goTo(index)
} else {
actionNavigateForward?.invoke(currentPage, pages[index])
goTo(index)
}
}
}
}
}
PageItem(disabled = currentPage.index == pages.lastIndex) {
PageLink(">") {
val nextIndex = currentPage.index + 1
actionNavigateForward?.invoke(currentPage, pages[nextIndex])
goTo(nextIndex)
}
}
}
}
if (entriesPerPageLimit is MutableState<Int>) {
val initLimit = remember { entriesPerPageLimit!!.value }
Column(styling = {
Margins {
End {
size = SpacingSpecs.SpacingSize.Auto
}
}
}, auto = true) {
DropDown("# ${entriesPerPageLimit!!.value}", size = ButtonSize.Small) {
List(4) {
initLimit * (it + 1)
}.forEach {
this.Button("$it") {
(entriesPerPageLimit as MutableState<Int>).value = it
}
}
}
}
}
}
}
public class OffsetPagination<T>(
data: List<T>,
public override val entriesPerPageLimit: State<Int>?,
public override val startPageIndex: Int = 0,
public override val position: Pagination.Position? = Pagination.Position.Bottom,
public override val numberOfButtons: Int = 5,
public override val actionNavigateBack: ((CurrentPage<T>, Pagination.Page<T>) -> Unit)? = null,
public override val actionNavigateForward: ((CurrentPage<T>, Pagination.Page<T>) -> Unit)? = null,
) : Pagination<T> {
override val pages: State<List<Pagination.Page<T>>> =
data.chunked(entriesPerPageLimit?.value ?: data.size).let {
mutableStateOf(
it.mapIndexed { index, data ->
Pagination.Page(index, data, it.size)
}
)
}
override var control: PageControl<T> = defaultControl()
public constructor(
data: List<T>,
entriesPerPageLimit: State<Int>,
startPageIndex: Int = 0,
position: Pagination.Position = Pagination.Position.Bottom,
numberOfButtons: Int = 5,
actionNavigateBack: ((CurrentPage<T>, Pagination.Page<T>) -> Unit)? = null,
actionNavigateForward: ((CurrentPage<T>, Pagination.Page<T>) -> Unit)? = null,
control: PageControl<T>
) : this(
data,
entriesPerPageLimit,
startPageIndex,
position,
numberOfButtons,
actionNavigateBack,
actionNavigateForward
) {
this.control = control
}
}
}
internal fun tableCalcButtons(index: Int, pages: Int, numberOfButtons: Int): IntRange {
if (pages <= numberOfButtons) {
return 0 until pages
}
val nr = min(pages, numberOfButtons)
val max = pages - 1
return when (index) {
0 -> 0 until nr
(pages - 1) -> (max((pages - nr), 0)) until pages
else -> {
val half = nr / 2
val lower = max(index - half, 0)
val upper = (min(index + half, max))
if (lower == 0) {
0 until nr
} else if (upper == max) {
val newLower = pages - numberOfButtons
newLower until pages
} else {
lower..upper
}
}
}
}
@Composable
@NonRestartableComposable
public fun <T> Table(
data: List<T>,
key: ((T) -> Any)? = null,
color: Color? = null,
stripedRows: Boolean = false,
stripedColumns: Boolean = false,
hover: Boolean = false,
borderless: Boolean = false,
small: Boolean = false,
fixedHeader: Table.FixedHeaderProperty? = null,
caption: ContentBuilder<HTMLTableCaptionElement>? = null,
captionTop: Boolean = false,
attrs: AttrBuilderContext<HTMLTableElement>? = null,
map: Table.Builder.(Int, T) -> Unit
) {
Table(
pagination = Table.OffsetPagination(data, null, position = null),
key,
color,
stripedRows = stripedRows,
stripedColumns = stripedColumns,
hover,
borderless,
small,
fixedHeader,
caption,
captionTop,
attrs,
map
)
}
@Composable
public fun <T> Table(
pagination: Table.Pagination<T>,
key: ((T) -> Any)? = null,
color: Color? = null,
stripedRows: Boolean = false,
stripedColumns: Boolean = false,
hover: Boolean = false,
borderless: Boolean = false,
small: Boolean = false,
fixedHeader: Table.FixedHeaderProperty? = null,
caption: ContentBuilder<HTMLTableCaptionElement>? = null,
captionTop: Boolean = false,
attrs: AttrBuilderContext<HTMLTableElement>? = null,
map: Table.Builder.(Int, T) -> Unit
) {
Style
val headers = mutableMapOf<String, Table.Header>()
val _footers = mutableListOf<Table.Footer>()
val pages by pagination.pages
var currentIndex by remember { mutableStateOf(pagination.startPageIndex) }
val currentPage = pages[min(currentIndex, pages.lastIndex)]
val baseIndex = currentPage.index * (pagination.entriesPerPageLimit?.value ?: 0)
val rows = currentPage.items.mapIndexed { itemIndexOfPage, item ->
val (columns, rowColor) = Table.Builder().apply {
val index = baseIndex + itemIndexOfPage
map(index, item)
}.build()
val cells = columns.map {
headers[it.title] = it.header
if (it.footer != null) {
_footers.add(it.footer)
}
it.cell
}
Table.Row(color = rowColor, cells = cells, key = key?.invoke(item))
}
check(rows.all { it.cells.size == headers.size })
val footers = _footers.takeUnless { it.isEmpty() }
if (footers != null) {
check(rows.all { it.cells.size == footers.size })
}
if (pagination.position == Table.Pagination.Position.Top) {
Row {
val control = pagination.control
control(pages, currentPage) {
currentIndex = it
}
}
}
Table(attrs = {
classes("table")
if (captionTop) {
classes("caption-top")
}
if (small) {
classes("table-sm")
}
color?.let { classes("table-$it") }
if (hover) {
classes("table-hover")
}
if (stripedRows) {
classes("table-striped")
}
if (stripedColumns) {
classes("table-striped-columns")
}
if (borderless) {
classes("table-borderless")
}
attrs?.invoke(this)
}) {
if (caption != null) {
Caption(content = caption)
}
Thead {
Tr {
headers.forEach { (_, header) ->
Th(attrs = {
scope(Scope.Col)
header.attrs?.invoke(this)
if (fixedHeader != null) {
classes("sticky-top")
style {
fixedHeader.style(this)
}
}
}) {
header.content?.invoke(this)
}
}
}
}
Tbody {
for (row in rows) {
key(row.key) {
Tr(attrs = {
row.color?.let { classes("table-$it") }
}) {
for (cell in row.cells) {
Td(attrs = {
cell.color?.let { classes("table-$it") }
cell.scope?.let { scope(it) }
cell.verticalAlignment?.let { classes(it.toString()) }
}) {
cell.content(this)
}
}
}
}
}
}
if (footers != null) {
Tfoot {
Tr {
footers.forEachIndexed { index, cell ->
Td(attrs = {
cell.color?.let { classes("table-$it") }
}) {
cell.content(this, rows[index].cells)
}
}
}
}
}
}
if (pagination.position == Table.Pagination.Position.Bottom) {
Row {
val control = pagination.control
control(pages, currentPage) {
currentIndex = it
}
}
}
}

View File

@ -0,0 +1,184 @@
package bootstrap
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateListOf
import com.benasher44.uuid.Uuid
import com.benasher44.uuid.uuid4
import org.jetbrains.compose.web.attributes.AttrsScope
import org.jetbrains.compose.web.attributes.ButtonType
import org.jetbrains.compose.web.attributes.type
import org.jetbrains.compose.web.dom.Button
import org.jetbrains.compose.web.dom.ContentBuilder
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Text
import org.w3c.dom.HTMLButtonElement
import org.w3c.dom.HTMLDivElement
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@Composable
public fun ToastContainer(
toastContainerState: ToastContainerState,
attrs: (AttrsScope<HTMLDivElement>.() -> Unit)? = null,
) {
Div(attrs = {
classes("toast-container")
attrs?.invoke(this)
}) {
toastContainerState.toasts.forEach { toastItem ->
key(toastItem) {
Toast(toastItem)
}
}
}
}
@Composable
private fun Toast(message: ToastContainerState.ToastItem) {
Style
needsJS
Div(
attrs = {
classes("toast")
attr("role", "alert")
attr("data-bs-delay", message.delay.inWholeMilliseconds.toString())
attr("aria-live", "assertive")
attr("aria-atomic", "true")
message.toastAttrs?.invoke(this)
}
) {
DisposableEffect(message) {
val htmlDivElement = scopeElement
val bsToast = Toast(htmlDivElement)
htmlDivElement.addEventListener("shown.bs.toast", callback = {
})
htmlDivElement.addEventListener("hidden.bs.toast", callback = {
message.remove()
})
bsToast.show()
onDispose {
bsToast.dispose()
message.remove()
htmlDivElement.removeEventListener("shown.bs.toast", callback = {
})
htmlDivElement.removeEventListener("hidden.bs.toast", callback = {
message.remove()
})
}
}
message.header?.let { header ->
Div(attrs = {
classes("toast-header")
}) {
header()
if (message.withDismissButton) {
DismissButton(attrs = message.dismissButtonAttrs)
}
}
}
if (message.header == null && message.withDismissButton) {
Div(attrs = { classes("d-flex") }) {
Div(attrs = { classes("toast-body") }, content = message.body)
DismissButton(styling = {
Margins {
End {
size = SpacingSpecs.SpacingSize.Small
}
All {
size = SpacingSpecs.SpacingSize.Auto
}
}
}) {
message.dismissButtonAttrs?.invoke(this)
}
}
} else {
Div(attrs = { classes("toast-body") }, content = message.body)
}
}
}
@Composable
private fun DismissButton(
styling: (Styling.() -> Unit)? = null,
attrs: (AttrsScope<HTMLButtonElement>.() -> Unit)? = null,
) {
val style = styling?.let {
Styling().apply(it).generate()
}
Button(attrs = {
type(ButtonType.Button)
classes("btn-close")
attr("data-bs-dismiss", "toast")
attr("aria-label", "close")
if (style != null) {
classes(classes = style)
}
attrs?.invoke(this)
})
}
public class ToastContainerState {
internal val toasts = mutableStateListOf<ToastItem>()
public fun showToast(toastMessage: String) {
showToast(body = {
Text(toastMessage)
})
}
/**
* Shows a new Toast.
* @param header Composable to generate the header content
* @param body Composable to generate the body content
* @param withDismissButton Set to True to show a default dismiss button icon, and false to not.
* You can also choose to include your own dismiss button in the [header] or [body] content
* instead of in addition to the default.
*
* @return A function the caller can invoke to dismiss the toast, for example, if you provide your own
* dismiss button in the header or body.
*/
public fun showToast(
withDismissButton: Boolean = true,
delay: Duration = 5.seconds,
toastAttrs: (AttrsScope<HTMLDivElement>.() -> Unit)? = null,
dismissButtonAttrs: (AttrsScope<HTMLButtonElement>.() -> Unit)? = null,
header: ContentBuilder<HTMLDivElement>? = null,
body: ContentBuilder<HTMLDivElement>,
): () -> Unit {
val uuid = uuid4()
val toastItem = ToastItem(
uuid,
delay = delay,
withDismissButton,
toastAttrs,
dismissButtonAttrs,
header = header,
body = body
) {
removeToast(uuid)
}
toasts.add(toastItem)
return { removeToast(uuid) }
}
private fun removeToast(uuid: Uuid) {
toasts.removeAll {
it.uuid == uuid
}
}
internal data class ToastItem(
val uuid: Uuid,
val delay: Duration,
val withDismissButton: Boolean,
val toastAttrs: (AttrsScope<HTMLDivElement>.() -> Unit)?,
val dismissButtonAttrs: (AttrsScope<HTMLButtonElement>.() -> Unit)?,
val header: ContentBuilder<HTMLDivElement>?,
val body: ContentBuilder<HTMLDivElement>?,
val remove: () -> Unit,
)
}

View File

@ -0,0 +1,34 @@
package bootstrap
import androidx.compose.runtime.Composable
import org.jetbrains.compose.web.dom.AttrBuilderContext
import org.jetbrains.compose.web.dom.Button
import org.jetbrains.compose.web.dom.Span
import org.w3c.dom.HTMLButtonElement
@Composable
public fun Toggler(
target: String,
controls: String,
styling: (Styling.() -> Unit)? = null,
attrs: AttrBuilderContext<HTMLButtonElement>? = null
) {
Style
val classes = styling?.let {
Styling().apply(it).generate()
}
Button(attrs = {
classes("navbar-toggler")
if (classes != null) {
classes(classes = classes)
}
attr("data-bs-toggle", "collapse")
attr("data-bs-target", "#$target")
attr("aria-controls", controls)
attr("aria-expanded", "false")
attr("aria-label", "Toggle navigation")
attrs?.invoke(this)
}) {
Span(attrs = { classes("navbar-toggler-icon") })
}
}

View File

@ -0,0 +1,18 @@
@file:Suppress(
"NAME_CONTAINS_ILLEGAL_CHARS",
"NESTED_CLASS_IN_EXTERNAL_INTERFACE",
"NOTHING_TO_INLINE",
)
package bootstrap
// language=JavaScript
@JsName("""(/*union*/{auto: 'auto'}/*union*/)""")
public sealed external interface ZIndex {
public companion object {
public val auto: ZIndex
}
}
public inline fun ZIndex(value: Int): ZIndex =
value.unsafeCast<ZIndex>()

View File

@ -0,0 +1,24 @@
@file:JsModule("bootstrap")
@file:JsNonModule
package bootstrap
import org.w3c.dom.HTMLDivElement
internal external class Modal(element: HTMLDivElement) {
internal fun show()
internal fun hide()
internal fun dispose()
}
internal external class Toast(element: HTMLDivElement) {
internal fun show()
internal fun dispose()
}
internal external class Offcanvas(element: HTMLDivElement) {
internal fun show()
internal fun hide()
}
internal external val needsJS: dynamic

View File

@ -0,0 +1,6 @@
@file:JsModule("@popperjs/core")
@file:JsNonModule
package bootstrap
internal external val needsPopper: dynamic

View File

@ -0,0 +1,5 @@
package bootstrap
@JsModule("bootstrap/scss/bootstrap.scss")
@JsNonModule
internal external val Style: dynamic

View File

@ -1,15 +1,18 @@
package space.kscience.visionforge.html
import androidx.compose.runtime.*
import app.softwork.bootstrapcompose.CloseButton
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
import org.jetbrains.compose.web.attributes.ButtonType
import org.jetbrains.compose.web.attributes.disabled
import org.jetbrains.compose.web.attributes.type
import org.jetbrains.compose.web.css.AlignItems
import org.jetbrains.compose.web.css.alignItems
import org.jetbrains.compose.web.css.px
import org.jetbrains.compose.web.css.width
import org.jetbrains.compose.web.dom.Button
import org.jetbrains.compose.web.dom.Div
import org.jetbrains.compose.web.dom.Span
import org.jetbrains.compose.web.dom.Text
@ -120,10 +123,16 @@ public fun PropertyEditor(
}
if (!name.isEmpty()) {
CloseButton(editorPropertyState != EditorPropertyState.Defined) {
rootMeta.remove(name)
update()
}
Button(attrs = {
type(ButtonType.Button)
if(editorPropertyState != EditorPropertyState.Defined) disabled()
classes("btn-close")
onClick {
rootMeta.remove(name)
update()
}
})
}
}
if (expanded) {

View File

@ -1,9 +1,9 @@
package space.kscience.visionforge.html
import androidx.compose.runtime.*
import app.softwork.bootstrapcompose.Card
import app.softwork.bootstrapcompose.NavbarLink
import app.softwork.bootstrapcompose.Styling
import bootstrap.Card
import bootstrap.NavbarLink
import bootstrap.Styling
import org.jetbrains.compose.web.css.overflowY
import org.jetbrains.compose.web.dom.*
import org.w3c.dom.HTMLAnchorElement

View File

@ -3,8 +3,7 @@
package space.kscience.visionforge.html
import androidx.compose.runtime.*
import kotlinx.uuid.UUID
import kotlinx.uuid.generateUUID
import com.benasher44.uuid.uuid4
import org.jetbrains.compose.web.attributes.*
import org.jetbrains.compose.web.dom.*
import org.w3c.dom.HTMLOptionElement
@ -45,7 +44,7 @@ public fun BooleanValueChooser(
value: Value?,
onValueChange: (Value?) -> Unit,
) {
val uid = remember { "checkbox[${UUID.generateUUID().toString(false)}]" }
val uid = remember { "checkbox[${uuid4()}]" }
var innerValue by remember(value, descriptor) {
mutableStateOf(
value?.boolean ?: descriptor?.defaultValue?.boolean

View File

@ -19,7 +19,7 @@ public interface VisionGroup : Vision {
override fun update(change: VisionChange) {
change.children?.forEach { (name, change) ->
if (change.vision != null || change.vision == NullVision) {
if (change.vision != null) {
error("VisionGroup is read-only")
} else {
children.getChild(name)?.update(change)

View File

@ -23,8 +23,8 @@ import space.kscience.visionforge.useProperty
public actual class MarkupPlugin : VisionPlugin(), ElementVisionRenderer {
public val visionClient: JsVisionClient by require(JsVisionClient)
override val tag: PluginTag get() = Companion.tag
override val visionSerializersModule: SerializersModule get() = markupSerializersModule
actual override val tag: PluginTag get() = Companion.tag
actual override val visionSerializersModule: SerializersModule get() = markupSerializersModule
override fun rateVision(vision: Vision): Int = when (vision) {
is VisionOfMarkup -> ElementVisionRenderer.DEFAULT_RATING
@ -57,9 +57,9 @@ public actual class MarkupPlugin : VisionPlugin(), ElementVisionRenderer {
}
public actual companion object : PluginFactory<MarkupPlugin> {
override val tag: PluginTag = PluginTag("vision.markup.js", PluginTag.DATAFORGE_GROUP)
actual override val tag: PluginTag = PluginTag("vision.markup.js", PluginTag.DATAFORGE_GROUP)
override fun build(context: Context, meta: Meta): MarkupPlugin = MarkupPlugin()
actual override fun build(context: Context, meta: Meta): MarkupPlugin = MarkupPlugin()
}
}

View File

@ -1,11 +1,9 @@
package space.kscience.visionforge.solid.three.compose
import androidx.compose.runtime.Composable
import app.softwork.bootstrapcompose.Button
import app.softwork.bootstrapcompose.Color.Info
import app.softwork.bootstrapcompose.Column
import app.softwork.bootstrapcompose.Layout.Height
import app.softwork.bootstrapcompose.Layout.Width
import bootstrap.Button
import bootstrap.Color
import bootstrap.Column
import org.jetbrains.compose.web.dom.Hr
import org.w3c.files.Blob
import org.w3c.files.BlobPropertyBag
@ -22,7 +20,7 @@ internal fun CanvasControls(
) {
Column {
vision?.let { vision ->
Button("Export", color = Info, styling = { Layout.width = Width.Full }) {
Button("Export", color = Color.Info, styling = { Layout.width = bootstrap.Layout.Width.Full }) {
val json = vision.encodeToString()
val fileSaver = kotlinext.js.require<dynamic>("file-saver")
@ -51,7 +49,7 @@ public fun ThreeControls(
) {
Tabs(
styling = {
Layout.height = Height.Full
Layout.height = bootstrap.Layout.Height.Full
}
) {
vision?.let { vision ->

View File

@ -1,11 +1,9 @@
package space.kscience.visionforge.solid.three.compose
import androidx.compose.runtime.*
import app.softwork.bootstrapcompose.Card
import app.softwork.bootstrapcompose.Column
import app.softwork.bootstrapcompose.Layout.Height
import app.softwork.bootstrapcompose.Layout.Width
import app.softwork.bootstrapcompose.Row
import bootstrap.Card
import bootstrap.Column
import bootstrap.Row
import kotlinx.dom.clear
import org.jetbrains.compose.web.css.*
import org.jetbrains.compose.web.dom.*
@ -83,15 +81,15 @@ public fun ThreeView(
Row(
styling = {
Layout {
width = Width.Full
height = Height.Full
width = bootstrap.Layout.Width.Full
height = bootstrap.Layout.Height.Full
}
}
) {
Column(
styling = {
Layout {
height = Height.Full
height = bootstrap.Layout.Height.Full
}
},
attrs = {
@ -166,7 +164,7 @@ public fun ThreeView(
auto = true,
styling = {
Layout {
height = Height.Full
height = bootstrap.Layout.Height.Full
}
},
attrs = {