├── .github └── workflows │ └── test.yml ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── misc.xml ├── uiDesigner.xml └── vcs.xml ├── ASSETS ├── accordion.gif ├── alerts.gif ├── androidIcon.png ├── badge.png ├── badgeButton.gif ├── breadcrumb.gif ├── buttons.png ├── carousel.gif ├── checkboxes.gif ├── closebutton.gif ├── collapse.gif ├── colorpicker.gif ├── custombuttons.gif ├── darkBackgroundDropdown.gif ├── disabledDropdown.gif ├── dropdowns.png ├── filepicker.gif ├── iconbutton.gif ├── inputs.gif ├── loadingButton.gif ├── loadingButtonText.gif ├── logo.svg ├── modal.gif ├── navbar.gif ├── navbar2.gif ├── offcanvas.gif ├── pagination.gif ├── placeholderDropdown.gif ├── progress.gif ├── radiobuttons.gif ├── range.gif ├── select.gif ├── spinner.gif ├── switches.gif ├── textarea.gif ├── toast.gif └── tooltip.gif ├── README.md ├── bootstrap ├── build.gradle.kts └── src │ └── jsMain │ └── kotlin │ └── com │ └── stevdza │ └── san │ └── kotlinbs │ ├── BootstrapApp.kt │ ├── components │ ├── BSAccordion.kt │ ├── BSAlert.kt │ ├── BSBadge.kt │ ├── BSBreadcrumb.kt │ ├── BSButton.kt │ ├── BSCarousel.kt │ ├── BSCloseButton.kt │ ├── BSCollapse.kt │ ├── BSDropdown.kt │ ├── BSIcon.kt │ ├── BSIconButton.kt │ ├── BSModal.kt │ ├── BSNavBar.kt │ ├── BSPagination.kt │ ├── BSProgress.kt │ ├── BSSpinner.kt │ ├── BSToast.kt │ ├── BSTooltip.kt │ ├── Offcanvas.kt │ ├── SpanText.kt │ └── SvgElement.kt │ ├── forms │ ├── BSCheckbox.kt │ ├── BSColorPicker.kt │ ├── BSFileInput.kt │ ├── BSInput.kt │ ├── BSRadioButton.kt │ ├── BSRange.kt │ ├── BSSelect.kt │ ├── BSSwitch.kt │ └── BSTextArea.kt │ ├── icons │ └── BSIcons.kt │ ├── models │ ├── AccordionItem.kt │ ├── AlertIcon.kt │ ├── AlertStyle.kt │ ├── BSBorderRadius.kt │ ├── BackgroundStyle.kt │ ├── BadgeVariant.kt │ ├── BorderColor.kt │ ├── BreadcrumbItem.kt │ ├── CarouselItem.kt │ ├── DropdownDirection.kt │ ├── InputSize.kt │ ├── InputValidation.kt │ ├── ModalSize.kt │ ├── PaginationButtons.kt │ ├── PaginationSize.kt │ ├── SelectSize.kt │ ├── SpinnerStyle.kt │ ├── SpinnerVariant.kt │ ├── ToastPlacement.kt │ ├── ToastStyle.kt │ ├── TooltipDirection.kt │ ├── button │ │ ├── ButtonBadge.kt │ │ ├── ButtonCustomization.kt │ │ ├── ButtonProperty.kt │ │ ├── ButtonSize.kt │ │ ├── ButtonType.kt │ │ └── ButtonVariant.kt │ ├── navbar │ │ ├── NavBarBrand.kt │ │ ├── NavBarButton.kt │ │ ├── NavBarExpand.kt │ │ ├── NavBarInputField.kt │ │ ├── NavBarOffcanvas.kt │ │ ├── NavDropdownItem.kt │ │ ├── NavItem.kt │ │ └── NavLink.kt │ └── offcanvas │ │ ├── OffcanvasBreakpoint.kt │ │ └── OffcanvasPlacement.kt │ └── util │ └── UniqueIdGenerator.kt ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── settings.gradle.kts └── site ├── .gitignore ├── .kobweb └── conf.yaml ├── build.gradle.kts └── src └── jsMain ├── kotlin └── com │ └── stevdza │ └── san │ └── kotlinbs │ ├── MyApp.kt │ └── pages │ └── Index.kt └── resources └── public └── favicon.ico /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Temp test kotlin npm install 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v3 12 | 13 | - name: Setup Java 14 | uses: actions/setup-java@v3 15 | with: 16 | distribution: temurin 17 | java-version: 11 18 | 19 | - name: Setup Gradle 20 | uses: gradle/gradle-build-action@v2 21 | 22 | - name: Make gradlew executable 23 | run: chmod +x gradlew 24 | 25 | - name: Test NPM install is working 26 | run: ./gradlew :kotlinNpmInstall 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General ignores 2 | .DS_Store 3 | build 4 | out 5 | kotlin-js-store 6 | 7 | # IntelliJ ignores 8 | *.iml 9 | /*.ipr 10 | 11 | /.idea/caches 12 | /.idea/libraries 13 | /.idea/modules.xml 14 | /.idea/workspace.xml 15 | /.idea/gradle.xml 16 | /.idea/navEditor.xml 17 | /.idea/assetWizardSettings.xml 18 | /.idea/artifacts 19 | /.idea/compiler.xml 20 | /.idea/jarRepositories.xml 21 | /.idea/*.iml 22 | /.idea/modules 23 | /.idea/libraries-with-intellij-classes.xml 24 | 25 | # Gradle ignores 26 | .gradle 27 | 28 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | kotlinbs -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ASSETS/accordion.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/accordion.gif -------------------------------------------------------------------------------- /ASSETS/alerts.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/alerts.gif -------------------------------------------------------------------------------- /ASSETS/androidIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/androidIcon.png -------------------------------------------------------------------------------- /ASSETS/badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/badge.png -------------------------------------------------------------------------------- /ASSETS/badgeButton.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/badgeButton.gif -------------------------------------------------------------------------------- /ASSETS/breadcrumb.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/breadcrumb.gif -------------------------------------------------------------------------------- /ASSETS/buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/buttons.png -------------------------------------------------------------------------------- /ASSETS/carousel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/carousel.gif -------------------------------------------------------------------------------- /ASSETS/checkboxes.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/checkboxes.gif -------------------------------------------------------------------------------- /ASSETS/closebutton.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/closebutton.gif -------------------------------------------------------------------------------- /ASSETS/collapse.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/collapse.gif -------------------------------------------------------------------------------- /ASSETS/colorpicker.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/colorpicker.gif -------------------------------------------------------------------------------- /ASSETS/custombuttons.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/custombuttons.gif -------------------------------------------------------------------------------- /ASSETS/darkBackgroundDropdown.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/darkBackgroundDropdown.gif -------------------------------------------------------------------------------- /ASSETS/disabledDropdown.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/disabledDropdown.gif -------------------------------------------------------------------------------- /ASSETS/dropdowns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/dropdowns.png -------------------------------------------------------------------------------- /ASSETS/filepicker.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/filepicker.gif -------------------------------------------------------------------------------- /ASSETS/iconbutton.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/iconbutton.gif -------------------------------------------------------------------------------- /ASSETS/inputs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/inputs.gif -------------------------------------------------------------------------------- /ASSETS/loadingButton.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/loadingButton.gif -------------------------------------------------------------------------------- /ASSETS/loadingButtonText.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/loadingButtonText.gif -------------------------------------------------------------------------------- /ASSETS/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /ASSETS/modal.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/modal.gif -------------------------------------------------------------------------------- /ASSETS/navbar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/navbar.gif -------------------------------------------------------------------------------- /ASSETS/navbar2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/navbar2.gif -------------------------------------------------------------------------------- /ASSETS/offcanvas.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/offcanvas.gif -------------------------------------------------------------------------------- /ASSETS/pagination.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/pagination.gif -------------------------------------------------------------------------------- /ASSETS/placeholderDropdown.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/placeholderDropdown.gif -------------------------------------------------------------------------------- /ASSETS/progress.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/progress.gif -------------------------------------------------------------------------------- /ASSETS/radiobuttons.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/radiobuttons.gif -------------------------------------------------------------------------------- /ASSETS/range.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/range.gif -------------------------------------------------------------------------------- /ASSETS/select.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/select.gif -------------------------------------------------------------------------------- /ASSETS/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/spinner.gif -------------------------------------------------------------------------------- /ASSETS/switches.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/switches.gif -------------------------------------------------------------------------------- /ASSETS/textarea.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/textarea.gif -------------------------------------------------------------------------------- /ASSETS/toast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/toast.gif -------------------------------------------------------------------------------- /ASSETS/tooltip.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/ASSETS/tooltip.gif -------------------------------------------------------------------------------- /bootstrap/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.varabyte.kobweb.gradle.core.util.importCss 2 | import com.varabyte.kobweb.gradle.library.util.configAsKobwebLibrary 3 | import kotlinx.html.script 4 | import kotlinx.html.style 5 | 6 | plugins { 7 | alias(libs.plugins.kotlin.multiplatform) 8 | alias(libs.plugins.kobweb.library) 9 | alias(libs.plugins.kotlin.compose) 10 | `maven-publish` 11 | } 12 | 13 | group = "com.stevdza.san.bootstrap" 14 | version = "0.1.6" 15 | 16 | kotlin { 17 | configAsKobwebLibrary(includeServer = false) 18 | 19 | sourceSets { 20 | commonMain.dependencies { 21 | implementation(libs.compose.runtime) 22 | } 23 | 24 | jsMain.dependencies { 25 | implementation(libs.compose.html.core) 26 | implementation(libs.kobweb.core) 27 | implementation(libs.kobweb.compose) 28 | implementation(libs.kobweb.silk.core) 29 | implementation(npm("bootstrap", "5.3.5")) 30 | } 31 | } 32 | } 33 | 34 | kobweb { 35 | library { 36 | index { 37 | head.add { 38 | script { 39 | src = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/js/bootstrap.bundle.min.js" 40 | } 41 | style { 42 | importCss( 43 | url = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.5/dist/css/bootstrap.min.css", 44 | layerName = "kotlin-bootstrap" 45 | ) 46 | } 47 | style { 48 | importCss( 49 | url = "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css", 50 | layerName = "kotlin-bootstrap" 51 | ) 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | publishing { 59 | publications { 60 | register("mavenJsLibrary", MavenPublication::class) { 61 | from(components["kotlin"]) 62 | groupId = "com.github.stevdza-san" 63 | artifactId = "KotlinBootstrap" 64 | version = version 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/BootstrapApp.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs 2 | 3 | import com.varabyte.kobweb.silk.init.InitSilk 4 | import com.varabyte.kobweb.silk.init.InitSilkContext 5 | import com.varabyte.kobweb.silk.style.layer.SilkLayer 6 | import com.varabyte.kobweb.silk.style.layer.add 7 | 8 | @InitSilk 9 | fun initBuildScriptLayers(ctx: InitSilkContext) { 10 | // Layer(s) referenced in build.gradle.kts 11 | ctx.stylesheet.cssLayers.add( 12 | "kotlin-bootstrap", 13 | after = SilkLayer.BASE 14 | ) 15 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/BSAccordion.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import com.stevdza.san.kotlinbs.models.AccordionItem 6 | import com.stevdza.san.kotlinbs.util.UniqueIdGenerator 7 | import com.varabyte.kobweb.compose.ui.Modifier 8 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 9 | import com.varabyte.kobweb.compose.ui.modifiers.id 10 | import com.varabyte.kobweb.compose.ui.thenIf 11 | import com.varabyte.kobweb.compose.ui.toAttrs 12 | import org.jetbrains.compose.web.dom.Button 13 | import org.jetbrains.compose.web.dom.Div 14 | import org.jetbrains.compose.web.dom.H2 15 | 16 | /** 17 | * UI element that allows you to create collapsible and expandable sections of content. 18 | * It is often used to display information in a compact and organized manner, 19 | * especially when you have a lot of content to present in a limited space. 20 | * @param id A unique identifier for the parent. 21 | * @param items One or multiple [AccordionItem]'s that represents the content that 22 | * you want to make expandable. 23 | * @param flush If set to true, it will remove some borders and rounded corners 24 | * to render accordions edge-to-edge with their parent container. 25 | * @param alwaysOpen If set to true, it will make accordion items stay open 26 | * when another item is opened. 27 | * */ 28 | @Composable 29 | fun BSAccordion( 30 | modifier: Modifier = Modifier, 31 | id: String? = null, 32 | items: List, 33 | flush: Boolean = false, 34 | alwaysOpen: Boolean = false 35 | ) { 36 | val randomId = remember { 37 | id ?: UniqueIdGenerator.generateUniqueId("accordion") 38 | } 39 | Div( 40 | attrs = modifier 41 | .id(randomId) 42 | .classNames("accordion") 43 | .thenIf( 44 | condition = flush, 45 | other = Modifier.classNames("accordion-flush") 46 | ) 47 | .toAttrs() 48 | ) { 49 | items.forEachIndexed { index, accordionItem -> 50 | val newRandomId = UniqueIdGenerator.generateUniqueId("$index-accordion") 51 | Div(attrs = Modifier.classNames("accordion-item").toAttrs()) { 52 | H2( 53 | attrs = Modifier 54 | .id("header-${randomId}") 55 | .classNames("accordion-header") 56 | .toAttrs() 57 | ) { 58 | Button(attrs = Modifier 59 | .classNames("accordion-button") 60 | .thenIf( 61 | condition = !accordionItem.defaultOpened, 62 | other = Modifier.classNames("collapsed") 63 | ) 64 | .toAttrs { 65 | attr("type", "button") 66 | attr("data-bs-toggle", "collapse") 67 | attr("data-bs-target", "#collapse-${newRandomId}") 68 | attr("aria-expanded", "true") 69 | attr("aria-controls", "collapse-${newRandomId}") 70 | } 71 | ) { 72 | SpanText(text = accordionItem.title) 73 | } 74 | } 75 | Div( 76 | attrs = Modifier 77 | .id("collapse-${newRandomId}") 78 | .classNames("accordion-collapse", "collapse") 79 | .thenIf( 80 | condition = accordionItem.defaultOpened, 81 | other = Modifier.classNames("show") 82 | ) 83 | .toAttrs { 84 | attr("aria-labelledby", "header-${randomId}") 85 | if (!alwaysOpen) attr("data-bs-parent", "#${randomId}") 86 | } 87 | ) { 88 | Div(attrs = Modifier.classNames("accordion-body").toAttrs()) { 89 | accordionItem.content() 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/BSAlert.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.stevdza.san.kotlinbs.models.AlertIcon 5 | import com.stevdza.san.kotlinbs.models.AlertStyle 6 | import com.varabyte.kobweb.compose.css.FontWeight 7 | import com.varabyte.kobweb.compose.ui.Modifier 8 | import com.varabyte.kobweb.compose.ui.attrsModifier 9 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 10 | import com.varabyte.kobweb.compose.ui.modifiers.fontWeight 11 | import com.varabyte.kobweb.compose.ui.modifiers.height 12 | import com.varabyte.kobweb.compose.ui.modifiers.width 13 | import com.varabyte.kobweb.compose.ui.thenIf 14 | import com.varabyte.kobweb.compose.ui.toAttrs 15 | import org.jetbrains.compose.web.css.px 16 | import org.jetbrains.compose.web.dom.A 17 | import org.jetbrains.compose.web.dom.Button 18 | import org.jetbrains.compose.web.dom.Div 19 | import org.jetbrains.compose.web.dom.Text 20 | 21 | /** 22 | * UI element that provides a way to display important messages, notifications, 23 | * or alerts to users. It is commonly used to convey information, warnings, errors, 24 | * or success messages in a visually prominent and attention-grabbing manner. 25 | * @param message A simple text that will be displayed inside an Alert dialog. 26 | * @param icon An [AlertIcon] that is used to give a specific context to the message. 27 | * @param dismissible Parameter that indicates whether an Alert can be dismissed or not. 28 | * @param style [AlertStyle] can change the look of the Alert itself. 29 | * */ 30 | @Composable 31 | fun BSAlert( 32 | modifier: Modifier = Modifier, 33 | message: String, 34 | icon: AlertIcon? = null, 35 | dismissible: Boolean = false, 36 | style: AlertStyle = AlertStyle.Primary 37 | ) { 38 | Div(attrs = modifier 39 | .classNames( 40 | *style.classes.toTypedArray(), 41 | "d-flex", 42 | "align-items-center" 43 | ) 44 | .thenIf( 45 | condition = dismissible, 46 | other = Modifier.classNames("alert-dismissible", "fade", "show") 47 | ) 48 | .toAttrs { 49 | attr("role", "alert") 50 | } 51 | ) { 52 | if (icon != null) { 53 | Svg(attrs = Modifier 54 | .width(24.px) 55 | .height(24.px) 56 | .classNames( 57 | "bi", 58 | icon.classes, 59 | "flex-shrink-0", 60 | "me-2" 61 | ) 62 | .toAttrs { 63 | attr("fill", "currentColor") 64 | attr("viewBox", "0 0 16 16") 65 | attr("role", "img") 66 | attr("aria-label", icon.label) 67 | } 68 | ) { 69 | Path(attrs = Modifier.toAttrs { 70 | attr("d", icon.path) 71 | }) 72 | } 73 | } 74 | Div { 75 | Text(value = message) 76 | } 77 | if (dismissible) { 78 | BSCloseButton( 79 | modifier = Modifier 80 | .attrsModifier { 81 | attr("data-bs-dismiss", "alert") 82 | } 83 | ) 84 | } 85 | } 86 | } 87 | 88 | /** 89 | * UI element that provides a way to display important messages, notifications, 90 | * or alerts to users. It is commonly used to convey information, warnings, errors, 91 | * or success messages in a visually prominent and attention-grabbing manner. 92 | * @param message A simple text that will be displayed inside an Alert dialog. 93 | * @param icon An [AlertIcon] that is used to give a specific context to the message. 94 | * @param dismissible Parameter that indicates whether an Alert can be dismissed or not. 95 | * @param style [AlertStyle] can change the look of the Alert itself. 96 | * @param alertLink This parameter allows you to add and mark a text from the message parameter 97 | * as a link [A]. The first string in the [Pair] represents the actual text from the message 98 | * that you want to link. While the second string in the [Pair] represents href/link which 99 | * you want to open when a user clicks on it. 100 | * */ 101 | @Composable 102 | fun BSAlert( 103 | modifier: Modifier = Modifier, 104 | message: String, 105 | icon: AlertIcon? = null, 106 | dismissible: Boolean = false, 107 | alertLink: Pair? = null, 108 | style: AlertStyle = AlertStyle.Primary 109 | ) { 110 | Div(attrs = modifier 111 | .classNames(*style.classes.toTypedArray()) 112 | .thenIf( 113 | condition = dismissible, 114 | other = Modifier.classNames("alert-dismissible", "fade", "show") 115 | ) 116 | .toAttrs { 117 | attr("role", "alert") 118 | } 119 | ) { 120 | if (icon != null) { 121 | Svg(attrs = Modifier 122 | .width(24.px) 123 | .height(24.px) 124 | .classNames( 125 | "bi", 126 | icon.classes, 127 | "flex-shrink-0", 128 | "me-2" 129 | ) 130 | .toAttrs { 131 | attr("fill", "currentColor") 132 | attr("viewBox", "0 0 16 16") 133 | attr("role", "img") 134 | attr("aria-label", icon.label) 135 | } 136 | ) { 137 | Path(attrs = Modifier.toAttrs { 138 | attr("d", icon.path) 139 | }) 140 | } 141 | } 142 | if (alertLink == null) { 143 | Text(value = message) 144 | } else { 145 | val messageParts = message.split(alertLink.first, ignoreCase = true) 146 | SpanText(text = messageParts.first()) 147 | A( 148 | attrs = Modifier.classNames("alert-link").toAttrs(), 149 | href = alertLink.second 150 | ) { 151 | SpanText(text = alertLink.first) 152 | } 153 | SpanText(text = messageParts.last()) 154 | } 155 | if (dismissible) { 156 | Button( 157 | attrs = Modifier 158 | .classNames("btn-close") 159 | .toAttrs { 160 | attr("type", "button") 161 | attr("data-bs-dismiss", "alert") 162 | attr("aria-label", "Close") 163 | } 164 | ) 165 | } 166 | } 167 | } 168 | 169 | /** 170 | * UI element that provides a way to display important messages, notifications, 171 | * or alerts to users. It is commonly used to convey information, warnings, errors, 172 | * or success messages in a visually prominent and attention-grabbing manner. 173 | * @param message A simple text that will be displayed inside an Alert dialog. 174 | * @param icon An [AlertIcon] that is used to give a specific context to the message. 175 | * @param dismissible Parameter that indicates whether an Alert can be dismissed or not. 176 | * @param bold Allows you to mark a specific text from a message parameter, 177 | * as a Bold. 178 | * @param style [AlertStyle] can change the look of the Alert itself. 179 | * */ 180 | @Composable 181 | fun BSAlert( 182 | modifier: Modifier = Modifier, 183 | message: String, 184 | icon: AlertIcon? = null, 185 | dismissible: Boolean = false, 186 | bold: String? = null, 187 | style: AlertStyle = AlertStyle.Primary 188 | ) { 189 | Div(attrs = modifier 190 | .classNames(*style.classes.toTypedArray()) 191 | .thenIf( 192 | condition = dismissible, 193 | other = Modifier.classNames("alert-dismissible", "fade", "show") 194 | ) 195 | .toAttrs { 196 | attr("role", "alert") 197 | } 198 | ) { 199 | if (icon != null) { 200 | Svg(attrs = Modifier 201 | .width(24.px) 202 | .height(24.px) 203 | .classNames( 204 | "bi", 205 | icon.classes, 206 | "flex-shrink-0", 207 | "me-2" 208 | ) 209 | .toAttrs { 210 | attr("fill", "currentColor") 211 | attr("viewBox", "0 0 16 16") 212 | attr("role", "img") 213 | attr("aria-label", icon.label) 214 | } 215 | ) { 216 | Path(attrs = Modifier.toAttrs { 217 | attr("d", icon.path) 218 | }) 219 | } 220 | } 221 | if (bold == null) { 222 | Text(value = message) 223 | } else { 224 | val messageParts = message.split(bold, ignoreCase = true) 225 | SpanText(text = messageParts.first()) 226 | SpanText( 227 | modifier = Modifier.fontWeight(FontWeight.Bold), 228 | text = bold 229 | ) 230 | SpanText(text = messageParts.last()) 231 | } 232 | if (dismissible) { 233 | Button( 234 | attrs = Modifier 235 | .classNames("btn-close") 236 | .toAttrs { 237 | attr("type", "button") 238 | attr("data-bs-dismiss", "alert") 239 | attr("aria-label", "Close") 240 | } 241 | ) 242 | } 243 | } 244 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/BSBadge.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.stevdza.san.kotlinbs.models.BackgroundStyle 5 | import com.stevdza.san.kotlinbs.models.BadgeVariant 6 | import com.varabyte.kobweb.compose.css.CSSLengthOrPercentageNumericValue 7 | import com.varabyte.kobweb.compose.css.FontWeight 8 | import com.varabyte.kobweb.compose.ui.Modifier 9 | import com.varabyte.kobweb.compose.ui.modifiers.* 10 | import com.varabyte.kobweb.compose.ui.thenIf 11 | import com.varabyte.kobweb.compose.ui.toAttrs 12 | import org.jetbrains.compose.web.css.px 13 | import org.jetbrains.compose.web.dom.Span 14 | 15 | /** 16 | * Badge is a small element used to highlight or display additional information related 17 | * to content. Badges are typically used to indicate the count of items, 18 | * notifications, statuses, or any other relevant information. 19 | * @param text Text that appears on top of the Badge. 20 | * @param fontFamily Font family of the text. 21 | * @param fontSize Size of the text. 22 | * @param fontWeight Font weight of the text. 23 | * @param style Different style that you can apply on this element. 24 | * @param variant Different variants of this component. 25 | * */ 26 | @Composable 27 | fun BSBadge( 28 | modifier: Modifier = Modifier, 29 | text: String, 30 | fontFamily: String? = null, 31 | fontSize: CSSLengthOrPercentageNumericValue? = null, 32 | fontWeight: FontWeight? = null, 33 | style: BackgroundStyle = BackgroundStyle.Primary, 34 | variant: BadgeVariant = BadgeVariant.Regular 35 | ) { 36 | Span( 37 | attrs = modifier 38 | .classNames(style.value) 39 | .thenIf( 40 | condition = variant != BadgeVariant.Regular && variant != BadgeVariant.Straight, 41 | other = variant.classes?.let { Modifier.classNames(*it.toTypedArray()) } ?: Modifier 42 | ) 43 | .thenIf( 44 | condition = variant != BadgeVariant.Empty, 45 | other = Modifier.classNames("badge") 46 | ) 47 | .thenIf( 48 | condition = variant == BadgeVariant.Straight, 49 | other = Modifier.borderRadius(0.px) 50 | ) 51 | .toAttrs() 52 | ) { 53 | if (variant != BadgeVariant.Empty) { 54 | SpanText( 55 | modifier = Modifier 56 | .thenIf( 57 | condition = fontFamily != null, 58 | other = fontFamily?.let { Modifier.fontFamily(it) } ?: Modifier 59 | ) 60 | .thenIf( 61 | condition = fontSize != null, 62 | other = fontSize?.let { Modifier.fontSize(it) } ?: Modifier 63 | ) 64 | .thenIf( 65 | condition = fontWeight != null, 66 | other = fontWeight?.let { Modifier.fontWeight(it) } ?: Modifier 67 | ), 68 | text = text 69 | ) 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/BSBreadcrumb.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.stevdza.san.kotlinbs.models.BreadcrumbItem 5 | import com.varabyte.kobweb.compose.ui.Modifier 6 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 7 | import com.varabyte.kobweb.compose.ui.thenIf 8 | import com.varabyte.kobweb.compose.ui.toAttrs 9 | import org.jetbrains.compose.web.dom.* 10 | 11 | /** 12 | * This component is used to create a navigation trail that indicates the user's current 13 | * location within a website's hierarchy. It consists of a series of links or text elements 14 | * separated by a separator character (typically a forward slash ("/")). 15 | * 16 | * The purpose of the breadcrumb component is to provide users with a way to understand 17 | * their current position within the website's structure and easily navigate back 18 | * to previous pages or sections. 19 | * @param items One or multiple [BreadcrumbItem]'s that allows you to specify a text 20 | * along with the link(href) which is used for navigation. 21 | * @param currentItem An item that is marked as selected. 22 | * @param divider A symbol which is used to separate [BreadcrumbItem]'s. 23 | * */ 24 | @Composable 25 | fun BSBreadcrumb( 26 | modifier: Modifier = Modifier, 27 | items: List, 28 | currentItem: String = items.first().text, 29 | divider: String = "/" 30 | ) { 31 | Nav(attrs = modifier 32 | .toAttrs { 33 | attr("aria-label", "breadcrumb") 34 | attr("style", "--bs-breadcrumb-divider: '$divider';") 35 | } 36 | ) { 37 | Ol( 38 | attrs = Modifier 39 | .classNames("breadcrumb") 40 | .toAttrs() 41 | ) { 42 | items.forEach { 43 | Li( 44 | attrs = Modifier 45 | .classNames("breadcrumb-item") 46 | .thenIf( 47 | condition = currentItem == it.text, 48 | other = Modifier.classNames("active") 49 | ) 50 | .toAttrs { 51 | if (currentItem == it.text) attr("aria-current", "page") 52 | } 53 | ) { 54 | if (currentItem != it.text) { 55 | A(href = it.href) { 56 | Text(value = it.text) 57 | } 58 | } else { 59 | Text(value = it.text) 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/BSButton.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import com.stevdza.san.kotlinbs.models.BSBorderRadius 6 | import com.stevdza.san.kotlinbs.models.button.ButtonBadge 7 | import com.stevdza.san.kotlinbs.models.button.ButtonCustomization 8 | import com.stevdza.san.kotlinbs.models.button.ButtonProperty 9 | import com.stevdza.san.kotlinbs.models.button.ButtonSize 10 | import com.stevdza.san.kotlinbs.models.button.ButtonType 11 | import com.stevdza.san.kotlinbs.models.button.ButtonVariant 12 | import com.stevdza.san.kotlinbs.util.UniqueIdGenerator 13 | import com.varabyte.kobweb.compose.ui.Modifier 14 | import com.varabyte.kobweb.compose.ui.modifiers.borderRadius 15 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 16 | import com.varabyte.kobweb.compose.ui.modifiers.id 17 | import com.varabyte.kobweb.compose.ui.modifiers.margin 18 | import com.varabyte.kobweb.compose.ui.modifiers.onClick 19 | import com.varabyte.kobweb.compose.ui.styleModifier 20 | import com.varabyte.kobweb.compose.ui.thenIf 21 | import com.varabyte.kobweb.compose.ui.toAttrs 22 | import org.jetbrains.compose.web.attributes.disabled 23 | import org.jetbrains.compose.web.css.px 24 | import org.jetbrains.compose.web.dom.Button 25 | import org.jetbrains.compose.web.dom.Span 26 | import org.jetbrains.compose.web.dom.Text 27 | 28 | /** 29 | * Buttons are used to trigger actions or events when clicked or tapped. 30 | * They are an essential part of any user interface as they provide a way for users 31 | * to interact with and control the application or website. 32 | * @param id A unique identifier of the button. 33 | * @param text Text that will appear on top of the button. 34 | * @param variant This one is used to stylize your button with a different color. 35 | * @param type Determine the behavior of the button when clicked or activated. 36 | * @param size The overall size of the button. 37 | * @param borderRadius The radius level of the button corners. 38 | * @param customization This is the place where you customize the button style all by 39 | * yourself. You will need to add styles of many properties, to handle different states. 40 | * If you have multiple buttons that will use this same custom style, it's usually 41 | * a good idea to create a top level variable that will hold the same [ButtonCustomization], 42 | * properties, so that it can be reused multiple times. 43 | * NOTE: When this parameter is specified, the [size], [variant] and [borderRadius] 44 | * parameters wil not work, because you're in full charge of the button customization. 45 | * @param disabled Whether a button is clickable or not. 46 | * @param loading When set to true, a loading indicator will appear. It's often used 47 | * with the other [loadingText] parameter. When loading is equal to 'true' the [onClick] 48 | * lambda is disabled. 49 | * @param loadingText Here you specify the text that will be shown, when the loading 50 | * state of the button changes to true. 51 | * @param badge Small badge or label, providing additional information or indicating 52 | * a specific status or count associated with the button. 53 | * @param icon Use 'BSIcons' object to choose one of many bootstrap icons inside the button. 54 | * @param onClick Lambda which is triggered everytime a user clicks on a button. 55 | * */ 56 | @Composable 57 | fun BSButton( 58 | modifier: Modifier = Modifier, 59 | id: String? = null, 60 | text: String, 61 | variant: ButtonVariant = ButtonVariant.Primary, 62 | type: ButtonType = ButtonType.Button, 63 | size: ButtonSize = ButtonSize.Default, 64 | borderRadius: BSBorderRadius? = null, 65 | customization: ButtonCustomization? = null, 66 | disabled: Boolean = false, 67 | loading: Boolean = false, 68 | loadingText: String? = null, 69 | badge: ButtonBadge? = null, 70 | icon: String? = null, 71 | onClick: () -> Unit 72 | ) { 73 | val randomId = remember { 74 | id ?: UniqueIdGenerator.generateUniqueId("button") 75 | } 76 | Button(attrs = Modifier 77 | .then(modifier) 78 | .id(randomId) 79 | .onClick { onClick() } 80 | .thenIf( 81 | condition = customization == null && borderRadius != null, 82 | other = borderRadius?.let { 83 | Modifier.borderRadius( 84 | topLeft = it.topLeft, 85 | topRight = borderRadius.topRight, 86 | bottomRight = borderRadius.bottomRight, 87 | bottomLeft = borderRadius.bottomLeft 88 | ) 89 | } ?: Modifier 90 | ) 91 | .thenIf( 92 | condition = customization == null, 93 | other = Modifier.classNames( 94 | *variant.classes.toTypedArray(), 95 | size.value 96 | ) 97 | ) 98 | .thenIf( 99 | condition = customization != null, 100 | other = Modifier.classNames(ButtonSize.Default.value) 101 | ) 102 | .classNames( 103 | "d-flex", 104 | "align-items-center", 105 | ) 106 | .thenIf( 107 | condition = badge != null, 108 | other = Modifier.classNames("position-relative") 109 | ) 110 | .styleModifier { 111 | if (customization != null) { 112 | customization.color?.let { property(ButtonProperty.COLOR, it) } 113 | customization.backgroundColor?.let { property(ButtonProperty.BACKGROUND_COLOR, it) } 114 | customization.borderColor?.let { property(ButtonProperty.BORDER_COLOR, it) } 115 | customization.hoverColor?.let { property(ButtonProperty.HOVER_COLOR, it) } 116 | customization.hoverBackgroundColor?.let { property(ButtonProperty.HOVER_BACKGROUND_COLOR, it) } 117 | customization.hoverBorderColor?.let { property(ButtonProperty.HOVER_BORDER_COLOR, it) } 118 | customization.activeColor?.let { property(ButtonProperty.ACTIVE_COLOR, it) } 119 | customization.activeBackgroundColor?.let { property(ButtonProperty.ACTIVE_BACKGROUND_COLOR, it) } 120 | customization.activeBorderColor?.let { property(ButtonProperty.ACTIVE_BORDER_COLOR, it) } 121 | customization.disabledColor?.let { property(ButtonProperty.DISABLED_COLOR, it) } 122 | customization.disabledBackgroundColor?.let { 123 | property( 124 | ButtonProperty.DISABLED_BACKGROUND_COLOR, 125 | it 126 | ) 127 | } 128 | customization.disabledBorderColor?.let { property(ButtonProperty.DISABLED_BORDER_COLOR, it) } 129 | 130 | property(ButtonProperty.BORDER_RADIUS, customization.borderRadius.value) 131 | property(ButtonProperty.HORIZONTAL_PADDING, customization.horizontalPadding) 132 | property(ButtonProperty.VERTICAL_PADDING, customization.verticalPadding) 133 | 134 | customization.fontFamily?.let { property(ButtonProperty.FONT_FAMILY, it) } 135 | property(ButtonProperty.FONT_SIZE, customization.fontSize) 136 | property(ButtonProperty.FONT_WEIGHT, customization.fontWeight.toString()) 137 | property(ButtonProperty.LINE_HEIGHT, customization.lineHeight.toString()) 138 | 139 | property(ButtonProperty.GRADIENT, customization.gradient.toString()) 140 | } 141 | } 142 | .toAttrs { 143 | attr("type", type.value) 144 | if (disabled || loading) disabled() 145 | } 146 | ) { 147 | if (badge != null) { 148 | BSBadge( 149 | modifier = badge.modifier 150 | .classNames( 151 | "position-absolute", 152 | "top-0", 153 | "start-100", 154 | "translate-middle" 155 | ), 156 | text = badge.text, 157 | style = badge.style, 158 | variant = badge.variant, 159 | ) 160 | } 161 | if (loading) { 162 | if (loadingText != null) { 163 | Span(attrs = Modifier 164 | .margin(right = 6.px) 165 | .classNames("spinner-border", "spinner-border-sm") 166 | .toAttrs { 167 | attr("role", "status") 168 | attr("aria-hidden", "true") 169 | } 170 | ) 171 | Text(value = loadingText) 172 | } else { 173 | Span(attrs = Modifier 174 | .classNames("spinner-border", "spinner-border-sm") 175 | .toAttrs { 176 | attr("role", "status") 177 | attr("aria-hidden", "true") 178 | } 179 | ) 180 | Span( 181 | attrs = Modifier 182 | .classNames("visually-hidden") 183 | .toAttrs() 184 | ) { 185 | Text(value = "Loading...") 186 | } 187 | } 188 | } else { 189 | if (icon != null) { 190 | BSIcon( 191 | modifier = Modifier.margin(right = 8.px), 192 | icon = icon 193 | ) 194 | SpanText(text = text) 195 | } else { 196 | SpanText(text = text) 197 | } 198 | } 199 | } 200 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/BSCarousel.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import com.stevdza.san.kotlinbs.models.CarouselItem 6 | import com.stevdza.san.kotlinbs.util.UniqueIdGenerator 7 | import com.varabyte.kobweb.compose.css.CSSLengthOrPercentageNumericValue 8 | import com.varabyte.kobweb.compose.css.ObjectFit 9 | import com.varabyte.kobweb.compose.ui.Modifier 10 | import com.varabyte.kobweb.compose.ui.attrsModifier 11 | import com.varabyte.kobweb.compose.ui.modifiers.* 12 | import com.varabyte.kobweb.compose.ui.thenIf 13 | import com.varabyte.kobweb.compose.ui.toAttrs 14 | import org.jetbrains.compose.web.dom.* 15 | 16 | /** 17 | * An interactive UI element that allows you to showcase a collection of images 18 | * in a sliding or fading manner. It is commonly used to create visually appealing and 19 | * dynamic slideshows or image galleries on web pages. 20 | * 21 | * The carousel component in Bootstrap provides a responsive and customizable carousel 22 | * container that can hold multiple slides. Each slide typically consists of an image 23 | * along with optional captions or additional content. The carousel automatically 24 | * transitions between slides, creating a smooth and visually engaging experience 25 | * for the user. 26 | * 27 | * @param id A unique identifier of the component. 28 | * @param items Single or multiple [CarouselItem]'s that will be shown into the carousel. 29 | * Each item contains a mandatory image parameter, and optional title and body. 30 | * @param width Width of the container. 31 | * @param height Height of the container. 32 | * @param showControls Whether to display controls or not. 33 | * @param showIndicators Whether to display indicators or not. 34 | * @param crossFade Whether to replace a sliding transition animation with a cross-fade. 35 | * @param dark If the value is true, controls and indicators will have dark colors. 36 | * Otherwise, their color will be white. 37 | * @param objectFit How an image should be resized and positioned within its container. 38 | * */ 39 | @Composable 40 | fun BSCarousel( 41 | modifier: Modifier = Modifier, 42 | id: String? = null, 43 | items: List, 44 | width: CSSLengthOrPercentageNumericValue, 45 | height: CSSLengthOrPercentageNumericValue, 46 | showControls: Boolean = true, 47 | showIndicators: Boolean = true, 48 | crossFade: Boolean = false, 49 | dark: Boolean = false, 50 | objectFit: ObjectFit = ObjectFit.Fill, 51 | ) { 52 | val randomId = remember { 53 | id ?: UniqueIdGenerator.generateUniqueId("carousel") 54 | } 55 | Div(attrs = modifier 56 | .id(randomId) 57 | .classNames("carousel", "slide") 58 | .width(width) 59 | .height(height) 60 | .thenIf( 61 | condition = crossFade, 62 | other = Modifier.classNames("carousel-fade") 63 | ) 64 | .thenIf( 65 | condition = dark, 66 | other = Modifier.classNames("carousel-dark") 67 | ) 68 | .toAttrs { 69 | attr("data-bs-ride", "carousel") 70 | } 71 | ) { 72 | if (showIndicators) { 73 | Div( 74 | attrs = Modifier 75 | .classNames("carousel-indicators") 76 | .toAttrs() 77 | ) { 78 | items.forEachIndexed { index, image -> 79 | Button(attrs = Modifier 80 | .thenIf( 81 | condition = image == items.first(), 82 | other = Modifier.classNames("active") 83 | ) 84 | .thenIf( 85 | condition = image == items.first(), 86 | other = Modifier.attrsModifier { 87 | attr("aria-current", "true") 88 | } 89 | ) 90 | .toAttrs { 91 | attr("data-bs-target", "#carouselExampleIndicators") 92 | attr("data-bs-slide-to", "$index") 93 | attr("data-bs-slide-to", "$index") 94 | attr("aria-label", "Slide $index") 95 | } 96 | ) 97 | } 98 | } 99 | } 100 | Div( 101 | attrs = Modifier 102 | .fillMaxSize() 103 | .classNames("carousel-inner") 104 | .toAttrs() 105 | ) { 106 | items.forEach { 107 | Div( 108 | attrs = Modifier 109 | .fillMaxSize() 110 | .classNames("carousel-item") 111 | .thenIf( 112 | condition = it == items.first(), 113 | other = Modifier.classNames("active") 114 | ) 115 | .toAttrs() 116 | ) { 117 | Img( 118 | src = it.image, 119 | alt = "Carousel Image", 120 | attrs = Modifier 121 | .fillMaxSize() 122 | .objectFit(objectFit) 123 | .classNames("d-block", "w-100") 124 | .toAttrs(), 125 | ) 126 | if (it.title != null || it.body != null) { 127 | Div( 128 | attrs = Modifier 129 | .classNames( 130 | "carousel-caption", 131 | "d-md-block", 132 | "d-none" 133 | ) 134 | .toAttrs() 135 | ) { 136 | H5 { 137 | it.title?.let { title -> Text(value = title) } 138 | } 139 | P { 140 | it.body?.let { body -> Text(value = body) } 141 | } 142 | } 143 | } 144 | } 145 | } 146 | if (showControls) { 147 | Button( 148 | attrs = Modifier 149 | .classNames("carousel-control-prev") 150 | .toAttrs { 151 | attr("type", "button") 152 | attr("data-bs-target", "#$randomId") 153 | attr("data-bs-slide", "prev") 154 | } 155 | ) { 156 | Span( 157 | attrs = Modifier 158 | .classNames("carousel-control-prev-icon") 159 | .toAttrs { 160 | attr("aria-hidden", "true") 161 | } 162 | ) 163 | Span( 164 | attrs = Modifier 165 | .classNames("visually-hidden") 166 | .toAttrs() 167 | ) { 168 | Text(value = "Previous") 169 | } 170 | } 171 | Button( 172 | attrs = Modifier 173 | .classNames("carousel-control-next") 174 | .toAttrs { 175 | attr("type", "button") 176 | attr("data-bs-target", "#$randomId") 177 | attr("data-bs-slide", "next") 178 | } 179 | ) { 180 | Span( 181 | attrs = Modifier 182 | .classNames("carousel-control-next-icon") 183 | .toAttrs { 184 | attr("aria-hidden", "true") 185 | } 186 | ) 187 | Span( 188 | attrs = Modifier 189 | .classNames("visually-hidden") 190 | .toAttrs() 191 | ) { 192 | Text(value = "Next") 193 | } 194 | } 195 | } 196 | } 197 | } 198 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/BSCloseButton.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.varabyte.kobweb.compose.ui.Modifier 5 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 6 | import com.varabyte.kobweb.compose.ui.thenIf 7 | import com.varabyte.kobweb.compose.ui.toAttrs 8 | import org.jetbrains.compose.web.attributes.disabled 9 | import org.jetbrains.compose.web.dom.Button 10 | 11 | /** 12 | * Pre-designed UI element that provides a visually identifiable button for closing 13 | * or dismissing a content block, alert, modal, or any other component that requires 14 | * user interaction to be closed. 15 | * @param disabled Whether this close button should be disabled. 16 | * @param dark Whether the color of this close button should be dark or not. 17 | * */ 18 | @Composable 19 | fun BSCloseButton( 20 | modifier: Modifier = Modifier, 21 | disabled: Boolean = false, 22 | dark: Boolean = true 23 | ) { 24 | Button(attrs = modifier 25 | .classNames("btn-close") 26 | .thenIf( 27 | condition = !dark, 28 | other = Modifier.classNames("btn-close-white") 29 | ) 30 | .toAttrs { 31 | attr("type", "button") 32 | attr("aria-label", "Close") 33 | if (disabled) disabled() 34 | } 35 | ) 36 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/BSCollapse.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.varabyte.kobweb.compose.ui.Modifier 5 | import com.varabyte.kobweb.compose.ui.attrsModifier 6 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 7 | import com.varabyte.kobweb.compose.ui.modifiers.id 8 | import com.varabyte.kobweb.compose.ui.toAttrs 9 | import org.jetbrains.compose.web.dom.Div 10 | 11 | /** 12 | * Toggle the visibility of your content. 13 | * Given how CSS handles animations, you cannot use padding on a content that 14 | * is collapsing. Instead, use the class as an independent wrapping element. 15 | * This component comes with a [showCollapse] util function, that is used to 16 | * trigger/show this component. 17 | * @param id A unique identifier of the component. 18 | * @param content The content that you're trying to hide. 19 | * */ 20 | @Composable 21 | fun BSCollapse( 22 | modifier: Modifier = Modifier, 23 | id: String, 24 | content: @Composable () -> Unit 25 | ) { 26 | Div( 27 | attrs = modifier 28 | .id(id) 29 | .classNames("collapse") 30 | .toAttrs() 31 | ) { 32 | content() 33 | } 34 | } 35 | 36 | /** 37 | * Util function which is used in a combination with [BSCollapse]. 38 | * */ 39 | fun Modifier.showCollapse(id: String): Modifier = attrsModifier { 40 | attr("data-bs-toggle", "collapse") 41 | attr("data-bs-target", "#$id") 42 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/BSDropdown.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.* 4 | import com.stevdza.san.kotlinbs.models.DropdownDirection 5 | import com.stevdza.san.kotlinbs.models.button.ButtonSize 6 | import com.stevdza.san.kotlinbs.models.button.ButtonVariant 7 | import com.varabyte.kobweb.compose.css.Cursor 8 | import com.varabyte.kobweb.compose.ui.Modifier 9 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 10 | import com.varabyte.kobweb.compose.ui.modifiers.cursor 11 | import com.varabyte.kobweb.compose.ui.modifiers.onClick 12 | import com.varabyte.kobweb.compose.ui.thenIf 13 | import com.varabyte.kobweb.compose.ui.toAttrs 14 | import org.jetbrains.compose.web.dom.* 15 | 16 | /** 17 | * A versatile UI element that allows you to create a menu or a list of options that can be 18 | * toggled to appear or disappear when triggered by user interaction. It provides a dropdown 19 | * menu that can contain other interactive elements. 20 | * @param items List of one or multiple strings that should be displayed within the dropdown. 21 | * @param selectedItem Item which is marked as selected in the drop-down, by default that's 22 | * a first item from the [items] list. 23 | * @param disabledItems List of one or multiple string items that should be displayed as 24 | * disabled within the dropdown. 25 | * @param style The style of the dropdown button. 26 | * @param size The size of the dropdown button. 27 | * @param direction Direction in which the dropdown should appear after it's opened. 28 | * @param splitToggleButton Whether the dropdown arrow should be presented as a separate 29 | * button within the dropdown button. 30 | * @param darkBackground Whether to display a dark background on a dropdown menu, when opened. 31 | * @param onItemSelect Lambda which is triggered when an item from the dropdown is clicked. 32 | * */ 33 | @Composable 34 | fun BSDropdown( 35 | modifier: Modifier = Modifier, 36 | items: List, 37 | selectedItem: String = items.first(), 38 | placeholder: String? = null, 39 | disabledItems: List? = null, 40 | style: ButtonVariant = ButtonVariant.Primary, 41 | size: ButtonSize = ButtonSize.Default, 42 | direction: DropdownDirection = DropdownDirection.Bottom, 43 | splitToggleButton: Boolean = false, 44 | darkBackground: Boolean = false, 45 | onItemSelect: (Int, String) -> Unit 46 | ) { 47 | var selectedItemInternal by remember(key1 = selectedItem) { mutableStateOf(selectedItem) } 48 | var isSelected by remember { mutableStateOf(false) } 49 | Div( 50 | attrs = modifier 51 | .classNames(if (splitToggleButton) "btn-group" else "dropdown") 52 | .thenIf( 53 | condition = direction != DropdownDirection.Bottom, 54 | other = Modifier.classNames(direction.value.toString()) 55 | ) 56 | .toAttrs() 57 | ) { 58 | if (splitToggleButton && direction == DropdownDirection.Left) { 59 | Button( 60 | attrs = Modifier 61 | .classNames( 62 | *style.classes.toTypedArray(), 63 | size.value, 64 | "dropdown-toggle", 65 | "dropdown-toggle-split" 66 | ) 67 | .toAttrs { 68 | attr("type", "button") 69 | attr("data-bs-toggle", "dropdown") 70 | } 71 | ) { 72 | Span(attrs = Modifier.classNames("visually-hidden").toAttrs()) { 73 | Text(value = "Toggle Dropdown") 74 | } 75 | } 76 | } 77 | Button( 78 | attrs = Modifier 79 | .classNames( 80 | *style.classes.toTypedArray(), 81 | size.value 82 | ) 83 | .thenIf( 84 | condition = !splitToggleButton, 85 | other = Modifier.classNames("dropdown-toggle") 86 | ) 87 | .toAttrs { 88 | attr("type", "button") 89 | if (!splitToggleButton) attr("data-bs-toggle", "dropdown") 90 | } 91 | ) { 92 | SpanText( 93 | if (placeholder != null) { 94 | if (isSelected) selectedItemInternal else placeholder 95 | } else selectedItemInternal 96 | ) 97 | } 98 | if (splitToggleButton && direction != DropdownDirection.Left) { 99 | Button( 100 | attrs = Modifier 101 | .classNames( 102 | *style.classes.toTypedArray(), 103 | size.value, 104 | "dropdown-toggle", 105 | "dropdown-toggle-split" 106 | ) 107 | .toAttrs { 108 | attr("type", "button") 109 | attr("data-bs-toggle", "dropdown") 110 | } 111 | ) { 112 | Span(attrs = Modifier.classNames("visually-hidden").toAttrs()) { 113 | Text(value = "Toggle Dropdown") 114 | } 115 | } 116 | } 117 | Ul( 118 | attrs = Modifier 119 | .classNames("dropdown-menu") 120 | .thenIf( 121 | condition = darkBackground, 122 | other = Modifier.classNames("dropdown-menu-dark") 123 | ) 124 | .toAttrs() 125 | ) { 126 | repeat(items.size) { index -> 127 | val isDisabled = disabledItems?.contains(items[index]) ?: false 128 | Li( 129 | attrs = Modifier 130 | .onClick { 131 | if (!isDisabled) { 132 | selectedItemInternal = items[index] 133 | onItemSelect(index, items[index]) 134 | isSelected = true 135 | } 136 | } 137 | .cursor(if (isDisabled) Cursor.NotAllowed else Cursor.Pointer) 138 | .toAttrs() 139 | ) { 140 | A( 141 | attrs = Modifier 142 | .classNames("dropdown-item") 143 | .thenIf( 144 | condition = selectedItemInternal == items[index] && (placeholder == null || isSelected), 145 | other = Modifier.classNames("active") 146 | ) 147 | .thenIf( 148 | condition = isDisabled, 149 | other = Modifier.classNames("disabled") 150 | ) 151 | .toAttrs { 152 | if (isDisabled) { 153 | attr("tabindex", "-1") 154 | attr("aria-disabled", "true") 155 | } 156 | } 157 | ) { 158 | SpanText(text = items[index]) 159 | } 160 | } 161 | } 162 | } 163 | } 164 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/BSIcon.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import com.stevdza.san.kotlinbs.icons.BSIcons 6 | import com.stevdza.san.kotlinbs.util.UniqueIdGenerator 7 | import com.varabyte.kobweb.compose.css.CSSLengthOrPercentageNumericValue 8 | import com.varabyte.kobweb.compose.ui.Modifier 9 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 10 | import com.varabyte.kobweb.compose.ui.modifiers.color 11 | import com.varabyte.kobweb.compose.ui.modifiers.fontSize 12 | import com.varabyte.kobweb.compose.ui.modifiers.id 13 | import com.varabyte.kobweb.compose.ui.thenIf 14 | import com.varabyte.kobweb.compose.ui.toAttrs 15 | import org.jetbrains.compose.web.css.CSSColorValue 16 | import org.jetbrains.compose.web.dom.I 17 | 18 | /** 19 | * A simple composable function used to represent an icon. 20 | * @param id A unique identifier of the button. 21 | * @param icon An object [BSIcons] which is used to specify an icon. 22 | * @param size The overall size of the icon, usually a default value is '1.cssRem'. 23 | * @param color The color of the icon. 24 | * */ 25 | @Composable 26 | fun BSIcon( 27 | modifier: Modifier = Modifier, 28 | id: String? = null, 29 | icon: String = BSIcons.CHECK, 30 | size: CSSLengthOrPercentageNumericValue? = null, 31 | color: CSSColorValue? = null 32 | ) { 33 | val randomId = remember { 34 | id ?: UniqueIdGenerator.generateUniqueId("icon") 35 | } 36 | I( 37 | attrs = modifier 38 | .id(randomId) 39 | .classNames(*icon.split(" ").toList().toTypedArray()) 40 | .thenIf( 41 | condition = size != null, 42 | other = size?.let { Modifier.fontSize(it) } ?: Modifier 43 | ) 44 | .thenIf( 45 | condition = color != null, 46 | other = color?.let { Modifier.color(it) } ?: Modifier 47 | ) 48 | .toAttrs() 49 | ) 50 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/BSIconButton.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import com.stevdza.san.kotlinbs.icons.BSIcons 6 | import com.stevdza.san.kotlinbs.models.BSBorderRadius 7 | import com.stevdza.san.kotlinbs.models.button.ButtonBadge 8 | import com.stevdza.san.kotlinbs.models.button.ButtonSize 9 | import com.stevdza.san.kotlinbs.models.button.ButtonVariant 10 | import com.stevdza.san.kotlinbs.util.UniqueIdGenerator 11 | import com.varabyte.kobweb.compose.ui.Modifier 12 | import com.varabyte.kobweb.compose.ui.modifiers.borderRadius 13 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 14 | import com.varabyte.kobweb.compose.ui.modifiers.id 15 | import com.varabyte.kobweb.compose.ui.modifiers.onClick 16 | import com.varabyte.kobweb.compose.ui.thenIf 17 | import com.varabyte.kobweb.compose.ui.toAttrs 18 | import org.jetbrains.compose.web.attributes.disabled 19 | import org.jetbrains.compose.web.dom.Button 20 | 21 | /** 22 | * This component is used to display an icon within a button, without any text. 23 | * @param id A unique identifier of the button. 24 | * @param icon An object [BSIcons] which is used to specify an icon. 25 | * @param size The overall size of the button. 26 | * @param variant This one is used to stylize your button with a different color. 27 | * @param borderRadius The radius level of the button corners. 28 | * @param disabled Whether a button is clickable or not. 29 | * @param badge Small badge or label, providing additional information or indicating 30 | * a specific status or count associated with the button. 31 | * @param onClick Lambda which is triggered everytime a user clicks on a button. 32 | * */ 33 | @Composable 34 | fun BSIconButton( 35 | modifier: Modifier = Modifier, 36 | id: String? = null, 37 | icon: String = BSIcons.CHECK, 38 | size: ButtonSize = ButtonSize.Default, 39 | variant: ButtonVariant = ButtonVariant.Primary, 40 | borderRadius: BSBorderRadius? = null, 41 | disabled: Boolean = false, 42 | badge: ButtonBadge? = null, 43 | onClick: () -> Unit 44 | ) { 45 | val randomId = remember { 46 | id ?: UniqueIdGenerator.generateUniqueId("iconButton") 47 | } 48 | Button( 49 | attrs = modifier 50 | .id(randomId) 51 | .classNames(*variant.classes.toTypedArray(), size.value) 52 | .thenIf( 53 | condition = borderRadius != null, 54 | other = borderRadius?.let { 55 | Modifier.borderRadius( 56 | topLeft = it.topLeft, 57 | topRight = it.topRight, 58 | bottomRight = it.bottomRight, 59 | bottomLeft = it.bottomLeft 60 | ) 61 | } ?: Modifier 62 | ) 63 | .thenIf( 64 | condition = badge != null, 65 | other = Modifier.classNames("position-relative") 66 | ) 67 | .onClick { onClick() } 68 | .toAttrs { 69 | attr("type", "button") 70 | attr("tabindex", "-1") 71 | if (disabled) disabled() 72 | } 73 | ) { 74 | if (badge != null) { 75 | BSBadge( 76 | modifier = badge.modifier 77 | .classNames( 78 | "position-absolute", 79 | "top-0", 80 | "start-100", 81 | "translate-middle" 82 | ), 83 | text = badge.text, 84 | style = badge.style, 85 | variant = badge.variant, 86 | ) 87 | } 88 | BSIcon(icon = icon) 89 | } 90 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/BSModal.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.stevdza.san.kotlinbs.models.button.ButtonVariant 5 | import com.stevdza.san.kotlinbs.models.ModalSize 6 | import com.varabyte.kobweb.compose.ui.Modifier 7 | import com.varabyte.kobweb.compose.ui.attrsModifier 8 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 9 | import com.varabyte.kobweb.compose.ui.modifiers.id 10 | import com.varabyte.kobweb.compose.ui.thenIf 11 | import com.varabyte.kobweb.compose.ui.toAttrs 12 | import org.jetbrains.compose.web.dom.Div 13 | import org.jetbrains.compose.web.dom.H2 14 | import org.jetbrains.compose.web.dom.Text 15 | 16 | /** 17 | * Powerful UI element used to display content, messages, or interactive forms in a popup 18 | * window that temporarily overlays the main content of a webpage. Modals are commonly used 19 | * to grab the user's attention and prompt them for an action, display additional information, 20 | * or confirm a choice. 21 | * This component comes with a [showModalOnClick] util function, that is used to trigger/show 22 | * this component. And [hideModalOnClick] that is used to dismiss the same component. 23 | * @param title Modal title. 24 | * @param body Modal body. 25 | * @param negativeButtonText Text of the negative button. 26 | * @param positiveButtonText Text of the positive button. 27 | * @param closableOutside Whether we can close a Modal when clicking somewhere outside. 28 | * @param centered Whether to center the content of the Modal. 29 | * @param size The size of the Modal itself. 30 | * @param onNegativeButtonClick Lambda which is triggered when a negative button is clicked. 31 | * @param onPositiveButtonClick Lambda which is triggered when a positive button is clicked. 32 | * */ 33 | @Composable 34 | fun BSModal( 35 | modifier: Modifier = Modifier, 36 | id: String, 37 | title: String, 38 | body: @Composable () -> Unit, 39 | negativeButtonText: String = "Close", 40 | positiveButtonText: String = "Okay", 41 | closableOutside: Boolean = false, 42 | centered: Boolean = true, 43 | size: ModalSize = ModalSize.None, 44 | onNegativeButtonClick: () -> Unit, 45 | onPositiveButtonClick: () -> Unit, 46 | ) { 47 | Div(attrs = modifier 48 | .id(id) 49 | .classNames("modal", "fade") 50 | .thenIf( 51 | condition = !closableOutside, 52 | other = Modifier.attrsModifier { 53 | attr("data-bs-backdrop", "static") 54 | } 55 | ) 56 | .toAttrs { 57 | attr("tabindex", "-1") 58 | } 59 | ) { 60 | Div( 61 | attrs = Modifier 62 | .classNames("modal-dialog") 63 | .thenIf( 64 | condition = size != ModalSize.None, 65 | other = Modifier.classNames(size.value) 66 | ) 67 | .thenIf( 68 | condition = centered, 69 | other = Modifier.classNames("modal-dialog-centered") 70 | ) 71 | .toAttrs() 72 | ) { 73 | Div( 74 | attrs = Modifier 75 | .classNames("modal-content") 76 | .toAttrs() 77 | ) { 78 | Div( 79 | attrs = Modifier 80 | .classNames("modal-header") 81 | .toAttrs() 82 | ) { 83 | H2( 84 | attrs = Modifier 85 | .classNames("modal-title") 86 | .toAttrs() 87 | ) { 88 | Text(value = title) 89 | } 90 | BSCloseButton(modifier = Modifier.attrsModifier { 91 | attr("data-bs-dismiss", "modal") 92 | }) 93 | } 94 | Div( 95 | attrs = Modifier 96 | .classNames("modal-body") 97 | .toAttrs() 98 | ) { 99 | body() 100 | } 101 | Div( 102 | attrs = Modifier 103 | .classNames("modal-footer") 104 | .toAttrs() 105 | ) { 106 | BSButton( 107 | modifier = Modifier.attrsModifier { 108 | attr("data-bs-dismiss", "modal") 109 | }, 110 | text = negativeButtonText, 111 | variant = ButtonVariant.Secondary, 112 | onClick = { onNegativeButtonClick() } 113 | ) 114 | BSButton( 115 | modifier = Modifier.attrsModifier { 116 | attr("data-bs-dismiss", "modal") 117 | }, 118 | text = positiveButtonText, 119 | variant = ButtonVariant.Primary, 120 | onClick = { onPositiveButtonClick() } 121 | ) 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | /** 129 | * Util function which is used to trigger/show [BSModal] component. 130 | * */ 131 | fun Modifier.showModalOnClick(id: String): Modifier = attrsModifier { 132 | attr("data-bs-toggle", "modal") 133 | attr("data-bs-target", "#$id") 134 | } 135 | 136 | /** 137 | * Util function which is used to hide [BSModal] component. 138 | * */ 139 | fun Modifier.hideModalOnClick(): Modifier = attrsModifier { 140 | attr("data-bs-dismiss", "modal") 141 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/BSNavBar.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.stevdza.san.kotlinbs.forms.BSInput 5 | import com.stevdza.san.kotlinbs.models.BackgroundStyle 6 | import com.stevdza.san.kotlinbs.models.navbar.* 7 | import com.varabyte.kobweb.compose.css.CSSLengthOrPercentageNumericValue 8 | import com.varabyte.kobweb.compose.css.Cursor 9 | import com.varabyte.kobweb.compose.foundation.layout.Column 10 | import com.varabyte.kobweb.compose.foundation.layout.Row 11 | import com.varabyte.kobweb.compose.ui.* 12 | import com.varabyte.kobweb.compose.ui.modifiers.* 13 | import org.jetbrains.compose.web.css.CSSNumeric 14 | import org.jetbrains.compose.web.css.px 15 | import org.jetbrains.compose.web.dom.* 16 | 17 | /** 18 | * UI element that helps create responsive navigation menus for websites or web applications. 19 | * The NavBar provides a consistent and user-friendly way to navigate between different 20 | * sections or pages of a website. 21 | * 22 | * The NavBar typically appears at the top of the web page and contains various navigation 23 | * elements such as links, buttons, dropdown menus, and branding elements like logos or 24 | * site names. It adapts to different screen sizes and devices, making it ideal for 25 | * responsive web design. 26 | * @param stickyTop Whether to make this component stickied on the top. 27 | * @param brand A [NavBarBrand] that allows you to specify a brand text, and optional 28 | * brand image as well. 29 | * @param items Currently there are two different [NavItem]'s that you can use. [NavLink] 30 | * that represents a simple link within this component, usually used to navigate between 31 | * different pages on your website. And the second one [NavDropdown] used to display a 32 | * dropdown menu item within this component. 33 | * @param itemsAlignment The alignment of the NavBar items. 34 | * @param inputField Use this parameter if you want to display an input field. 35 | * @param button An optional button. 36 | * @param offcanvas Use this parameter if you want to replace a default expanded 37 | * navBar with a [BSOffcanvas] (Side panel/Overflow panel). 38 | * @param expand This parameter allows you to specify when should your component 39 | * display a collapsed state. 40 | * @param horizontalPadding The padding on the left/right of the component. 41 | * @param backgroundStyle A background style of this component. 42 | * */ 43 | @Composable 44 | fun BSNavBar( 45 | modifier: Modifier = Modifier, 46 | stickyTop: Boolean = false, 47 | brand: NavBarBrand? = null, 48 | items: List, 49 | itemsAlignment: Alignment.Horizontal = Alignment.Start, 50 | inputField: NavBarInputField? = null, 51 | button: NavBarButton? = null, 52 | offcanvas: NavBarOffcanvas? = null, 53 | expand: NavBarExpand = NavBarExpand.LG, 54 | horizontalPadding: CSSLengthOrPercentageNumericValue = 8.px, 55 | backgroundStyle: BackgroundStyle = BackgroundStyle.Light 56 | ) { 57 | Nav( 58 | attrs = modifier 59 | .classNames( 60 | "navbar", 61 | expand.value, 62 | backgroundStyle.value 63 | ) 64 | .thenIf( 65 | condition = stickyTop, 66 | other = Modifier.classNames("sticky-top") 67 | ) 68 | .toAttrs { 69 | if (backgroundStyle == BackgroundStyle.Light || 70 | backgroundStyle == BackgroundStyle.Info || 71 | backgroundStyle == BackgroundStyle.Warning 72 | ) { 73 | attr("data-bs-theme", "light") 74 | } else { 75 | attr("data-bs-theme", "dark") 76 | } 77 | } 78 | ) { 79 | Div( 80 | attrs = Modifier 81 | .padding(leftRight = horizontalPadding) 82 | .classNames("container-fluid").toAttrs() 83 | ) { 84 | if (brand != null) { 85 | A( 86 | attrs = Modifier 87 | .classNames("navbar-brand") 88 | .toAttrs(), 89 | href = brand.href 90 | ) { 91 | Row( 92 | verticalAlignment = Alignment.CenterVertically 93 | ) { 94 | if (brand.image != null) { 95 | Img( 96 | src = brand.image, 97 | alt = "Brand Logo", 98 | attrs = Modifier 99 | .width(brand.imageWidth) 100 | .margin(right = 8.px) 101 | .classNames("d-inline-block", "align-text-top") 102 | .toAttrs() 103 | ) 104 | } 105 | SpanText(brand.title) 106 | } 107 | } 108 | } 109 | Button( 110 | attrs = Modifier 111 | .classNames("navbar-toggler") 112 | .toAttrs { 113 | if (offcanvas != null) { 114 | attr("data-bs-toggle", "offcanvas") 115 | attr("data-bs-target", "#${offcanvas.id}") 116 | } else { 117 | attr("data-bs-toggle", "collapse") 118 | attr("data-bs-target", "#navbarSupportedContent") 119 | } 120 | attr("aria-controls", "navbarSupportedContent") 121 | attr("aria-expanded", "false") 122 | attr("aria-label", "Toggle Navigation") 123 | } 124 | ) { 125 | Span(attrs = Modifier.classNames("navbar-toggler-icon").toAttrs()) 126 | } 127 | Div( 128 | attrs = Modifier 129 | .id("navbarSupportedContent") 130 | .classNames("collapse", "navbar-collapse") 131 | .toAttrs() 132 | ) { 133 | NavBarContent( 134 | items = items, 135 | itemsAlignment = itemsAlignment, 136 | inputField = inputField, 137 | button = button 138 | ) 139 | } 140 | } 141 | } 142 | if (offcanvas != null) { 143 | BSOffcanvas( 144 | id = offcanvas.id, 145 | title = offcanvas.title, 146 | body = { 147 | Column { 148 | NavBarContent( 149 | items = items, 150 | itemsAlignment = itemsAlignment, 151 | inputField = inputField, 152 | button = button, 153 | offcanvas = offcanvas 154 | ) 155 | } 156 | }, 157 | dark = offcanvas.dark, 158 | allowScrolling = offcanvas.allowScrolling, 159 | disableBackdrop = offcanvas.disableBackdrop, 160 | closableOutside = offcanvas.closableOutside, 161 | placement = offcanvas.placement 162 | ) 163 | } 164 | } 165 | 166 | @Composable 167 | private fun NavBarContent( 168 | items: List, 169 | itemsAlignment: Alignment.Horizontal, 170 | inputField: NavBarInputField?, 171 | button: NavBarButton?, 172 | offcanvas: NavBarOffcanvas? = null 173 | ) { 174 | Ul( 175 | attrs = Modifier 176 | .classNames( 177 | "navbar-nav", 178 | if (offcanvas != null) { 179 | "me-auto" 180 | } else { 181 | if (itemsAlignment is Alignment.Start) "me-auto" else if (itemsAlignment is Alignment.End) "ms-auto" else "mx-auto" 182 | } 183 | ) 184 | .toAttrs() 185 | ) { 186 | items.forEachIndexed { index, navItem -> 187 | if (navItem is NavDropdown) { 188 | Li( 189 | attrs = Modifier 190 | .classNames("nav-item", "dropdown") 191 | .toAttrs() 192 | ) { 193 | A( 194 | attrs = Modifier 195 | .classNames("nav-link", "dropdown-toggle") 196 | .toAttrs { 197 | attr("role", "button") 198 | attr("data-bs-toggle", "dropdown") 199 | attr("aria-expanded", "false") 200 | } 201 | ) { 202 | Text(value = navItem.placeholder) 203 | } 204 | Ul( 205 | attrs = Modifier.classNames("dropdown-menu").toAttrs() 206 | ) { 207 | navItem.items.forEachIndexed { index, dropdownItem -> 208 | Li( 209 | attrs = Modifier 210 | .id(dropdownItem.id) 211 | .onClick { 212 | dropdownItem.onClick(index) 213 | } 214 | .toAttrs() 215 | ) { 216 | A( 217 | attrs = Modifier 218 | .classNames("dropdown-item") 219 | .cursor(Cursor.Pointer) 220 | .toAttrs(), 221 | ) { Text(value = dropdownItem.title) } 222 | } 223 | } 224 | } 225 | } 226 | } else if (navItem is NavLink) { 227 | Li( 228 | attrs = Modifier 229 | .id(navItem.id) 230 | .classNames("nav-item") 231 | .onClick { navItem.onClick(index) } 232 | .toAttrs() 233 | ) { 234 | A( 235 | attrs = Modifier 236 | .classNames("nav-link") 237 | .thenIf( 238 | condition = navItem.active, 239 | other = Modifier.classNames("active") 240 | ) 241 | .thenIf( 242 | condition = navItem.disabled, 243 | other = Modifier.classNames("disabled") 244 | ) 245 | .cursor(Cursor.Pointer) 246 | .toAttrs { 247 | if (navItem.active) attr("aria-current", "page") 248 | } 249 | ) { 250 | Text(value = navItem.title) 251 | } 252 | } 253 | } 254 | } 255 | } 256 | Form( 257 | attrs = Modifier 258 | .classNames("d-flex") 259 | .toAttrs { 260 | attr("role", "search") 261 | } 262 | ) { 263 | if (inputField != null) { 264 | BSInput( 265 | modifier = Modifier 266 | .thenIf( 267 | condition = button != null, 268 | other = Modifier.margin(right = 8.px) 269 | ), 270 | id = inputField.id, 271 | placeholder = inputField.placeholder, 272 | value = inputField.value, 273 | onValueChange = inputField.onValueChange, 274 | onEnterClick = inputField.onEnterClick 275 | ) 276 | } 277 | if (button != null) { 278 | BSButton( 279 | id = button.id, 280 | text = button.text, 281 | variant = button.variant, 282 | disabled = button.disabled, 283 | loading = button.loading, 284 | loadingText = button.loadingText, 285 | badge = button.badge, 286 | onClick = button.onClick 287 | ) 288 | } 289 | } 290 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/BSPagination.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.* 4 | import com.stevdza.san.kotlinbs.models.NextButton 5 | import com.stevdza.san.kotlinbs.models.PaginationSize 6 | import com.stevdza.san.kotlinbs.models.PreviousButton 7 | import com.stevdza.san.kotlinbs.util.UniqueIdGenerator 8 | import com.varabyte.kobweb.compose.css.Cursor 9 | import com.varabyte.kobweb.compose.css.UserSelect 10 | import com.varabyte.kobweb.compose.ui.Modifier 11 | import com.varabyte.kobweb.compose.ui.modifiers.* 12 | import com.varabyte.kobweb.compose.ui.thenIf 13 | import com.varabyte.kobweb.compose.ui.toAttrs 14 | import org.jetbrains.compose.web.dom.A 15 | import org.jetbrains.compose.web.dom.Li 16 | import org.jetbrains.compose.web.dom.Nav 17 | import org.jetbrains.compose.web.dom.Ul 18 | 19 | /** 20 | * Pagination component is used to divide long lists or tables into multiple pages, 21 | * making it easier for users to navigate through the content. 22 | * @param id A unique identifier of the button. 23 | * @param pages A total number of pages. The minimum number of pages is 2. 24 | * @param currentPage Currently selected page. This will apply an active state to the page. 25 | * @param maxVisiblePages How many pages should be visible on the screen. 26 | * @param size The size of the [BSPagination] component. 27 | * @param previousButton Preview button when pressed, should decrement the [currentPage] value. 28 | * @param nextButton Next button when pressed, should increment the [currentPage] value. 29 | * @param onPageClick Lambda which is triggered when you click on a page. It holds the Int value 30 | * of the clicked page. 31 | * */ 32 | @Composable 33 | fun BSPagination( 34 | modifier: Modifier = Modifier, 35 | id: String? = null, 36 | pages: Int, 37 | currentPage: Int = 1, 38 | maxVisiblePages: Int = 3, 39 | size: PaginationSize = PaginationSize.Default, 40 | previousButton: PreviousButton? = null, 41 | nextButton: NextButton? = null, 42 | onPageClick: (Int) -> Unit 43 | ) { 44 | val randomId = remember { 45 | id ?: UniqueIdGenerator.generateUniqueId("pagination") 46 | } 47 | val numberOfPages = remember(pages) { if (pages > 1) 1..pages else 1..2 } 48 | val numberOfMaxVisiblePages = remember(maxVisiblePages) { if (maxVisiblePages < 2) 2 else maxVisiblePages } 49 | val currentPageInternal = remember(currentPage) { 50 | if (currentPage <= numberOfPages.last 51 | && currentPage >= numberOfPages.first 52 | ) currentPage 53 | else if (currentPage < 1) 1 54 | else numberOfPages.last 55 | } 56 | 57 | var previousButtonDisabled by remember(previousButton?.disabled) { mutableStateOf(previousButton?.disabled) } 58 | var nextButtonDisabled by remember(nextButton?.disabled) { mutableStateOf(nextButton?.disabled) } 59 | 60 | val currentPageIndex = remember(currentPageInternal) { numberOfPages.indexOf(currentPageInternal) } 61 | val endIndex = remember(currentPageIndex) { (currentPageIndex + numberOfMaxVisiblePages).coerceAtMost(numberOfPages.count()) } 62 | val visiblePages = remember(currentPageIndex) { 63 | numberOfPages.toList().subList( 64 | if ((endIndex - currentPageIndex) < numberOfMaxVisiblePages) 65 | currentPageIndex - (numberOfMaxVisiblePages - (endIndex - currentPageIndex)) 66 | else currentPageIndex, 67 | endIndex 68 | ) 69 | } 70 | 71 | LaunchedEffect(currentPageInternal) { 72 | previousButtonDisabled = currentPageInternal <= numberOfPages.first 73 | nextButtonDisabled = currentPageInternal >= numberOfPages.last 74 | } 75 | 76 | Nav( 77 | attrs = modifier 78 | .id(randomId) 79 | .toAttrs { 80 | attr("aria-label", "Pagination") 81 | } 82 | ) { 83 | Ul( 84 | attrs = Modifier 85 | .classNames(*size.classes.toTypedArray()) 86 | .toAttrs() 87 | ) { 88 | if (previousButton != null) { 89 | Li( 90 | attrs = Modifier 91 | .classNames("page-item") 92 | .thenIf( 93 | condition = previousButtonDisabled == true, 94 | other = Modifier.classNames("disabled") 95 | ) 96 | .onClick { 97 | if (currentPageInternal > numberOfPages.first) { 98 | previousButton.onClick(currentPageInternal - 1) 99 | } 100 | } 101 | .cursor(Cursor.Pointer) 102 | .toAttrs() 103 | ) { 104 | A( 105 | attrs = Modifier 106 | .classNames("page-link") 107 | .toAttrs { 108 | if (previousButtonDisabled == true) attr("tabindex", "-1") 109 | } 110 | ) { 111 | SpanText( 112 | modifier = Modifier.userSelect(UserSelect.None), 113 | text = previousButton.text 114 | ) 115 | } 116 | } 117 | } 118 | visiblePages.forEach { page -> 119 | Li( 120 | attrs = Modifier 121 | .classNames("page-item") 122 | .thenIf( 123 | condition = currentPageInternal == page, 124 | other = Modifier.classNames("active") 125 | ) 126 | .onClick { onPageClick(page) } 127 | .cursor(Cursor.Pointer) 128 | .toAttrs() 129 | ) { 130 | A( 131 | attrs = Modifier 132 | .classNames("page-link") 133 | .toAttrs() 134 | ) { 135 | SpanText( 136 | modifier = Modifier.userSelect(UserSelect.None), 137 | text = "$page" 138 | ) 139 | } 140 | } 141 | } 142 | if (nextButton != null) { 143 | Li( 144 | attrs = Modifier 145 | .classNames("page-item") 146 | .thenIf( 147 | condition = nextButtonDisabled == true, 148 | other = Modifier.classNames("disabled") 149 | ) 150 | .onClick { 151 | if (currentPageInternal < numberOfPages.last) { 152 | nextButton.onClick(currentPageInternal + 1) 153 | } 154 | } 155 | .cursor(Cursor.Pointer) 156 | .toAttrs() 157 | ) { 158 | A( 159 | attrs = Modifier 160 | .classNames("page-link") 161 | .toAttrs { 162 | if (nextButtonDisabled == true) attr("tabindex", "-1") 163 | } 164 | ) { 165 | SpanText( 166 | modifier = Modifier.userSelect(UserSelect.None), 167 | text = nextButton.text 168 | ) 169 | } 170 | } 171 | } 172 | } 173 | } 174 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/BSProgress.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.stevdza.san.kotlinbs.models.BackgroundStyle 5 | import com.varabyte.kobweb.compose.css.CSSLengthOrPercentageNumericValue 6 | import com.varabyte.kobweb.compose.ui.Modifier 7 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 8 | import com.varabyte.kobweb.compose.ui.modifiers.fillMaxWidth 9 | import com.varabyte.kobweb.compose.ui.modifiers.height 10 | import com.varabyte.kobweb.compose.ui.modifiers.width 11 | import com.varabyte.kobweb.compose.ui.thenIf 12 | import com.varabyte.kobweb.compose.ui.toAttrs 13 | import org.jetbrains.compose.web.css.* 14 | import org.jetbrains.compose.web.dom.Div 15 | import org.jetbrains.compose.web.dom.Text 16 | 17 | /** 18 | * A visual element used to display the progress or completion of a task or process. 19 | * It provides a horizontal bar that visually represents the current status or percentage 20 | * of completion for a particular operation. 21 | * @param label Progress label. 22 | * @param percentage Progress completion in percentage. From 0% to 100%. 23 | * @param height Progress height. 24 | * @param style Background style of progress. 25 | * @param striped Whether to use a stripped style. 26 | * @param stripedAnimated Whether to animate a striped style. 27 | * */ 28 | @Composable 29 | fun BSProgress( 30 | modifier: Modifier = Modifier.fillMaxWidth(), 31 | label: String? = null, 32 | percentage: CSSSizeValue = 50.percent, 33 | height: CSSLengthOrPercentageNumericValue = 20.px, 34 | width: CSSLengthOrPercentageNumericValue = 300.px, 35 | style: BackgroundStyle = BackgroundStyle.Primary, 36 | striped: Boolean = false, 37 | stripedAnimated: Boolean = false 38 | ) { 39 | Div( 40 | attrs = modifier 41 | .height(height) 42 | .width(width) 43 | .classNames("progress") 44 | .toAttrs() 45 | ) { 46 | Div( 47 | attrs = Modifier 48 | .classNames("progress-bar", style.value) 49 | .thenIf( 50 | condition = striped, 51 | other = Modifier.classNames("progress-bar-striped") 52 | ) 53 | .thenIf( 54 | condition = stripedAnimated, 55 | other = Modifier.classNames("progress-bar-striped", "progress-bar-animated") 56 | ) 57 | .width(percentage) 58 | .toAttrs { 59 | attr("role", "progressbar") 60 | attr("aria-label", "Progress Bar") 61 | attr("aria-valuenow", "${percentage.value}") 62 | attr("aria-valuemin", "0") 63 | attr("aria-valuemax", "100") 64 | } 65 | ) { 66 | label?.let { Text(value = it) } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/BSSpinner.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.stevdza.san.kotlinbs.models.SpinnerStyle 5 | import com.stevdza.san.kotlinbs.models.SpinnerVariant 6 | import com.varabyte.kobweb.compose.ui.Modifier 7 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 8 | import com.varabyte.kobweb.compose.ui.modifiers.size 9 | import com.varabyte.kobweb.compose.ui.thenIf 10 | import com.varabyte.kobweb.compose.ui.toAttrs 11 | import org.jetbrains.compose.web.css.cssRem 12 | import org.jetbrains.compose.web.dom.Div 13 | import org.jetbrains.compose.web.dom.Span 14 | import org.jetbrains.compose.web.dom.Text 15 | 16 | /** 17 | * Indicate that content is loading or processing. It provides an animated spinner that 18 | * rotates continuously, giving users a visual cue that an operation is in progress, and 19 | * they need to wait for the result. 20 | * @param style The visual style of the spinner. 21 | * @param variant A different variant of the spinner. 22 | * */ 23 | @Composable 24 | fun BSSpinner( 25 | modifier: Modifier = Modifier, 26 | style: SpinnerStyle = SpinnerStyle.Primary, 27 | variant: SpinnerVariant = SpinnerVariant.Default 28 | ) { 29 | Div(attrs = modifier 30 | .classNames( 31 | style.value, 32 | *variant.classes.toTypedArray() 33 | ) 34 | .thenIf( 35 | condition = variant is SpinnerVariant.Large || variant is SpinnerVariant.LargeGrow, 36 | other = Modifier.size(3.cssRem) 37 | ) 38 | .toAttrs { 39 | attr("role", "status") 40 | } 41 | ) { 42 | Span(attrs = Modifier.classNames("visually-hidden").toAttrs()) { 43 | Text(value = "Loading...") 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/BSToast.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.stevdza.san.kotlinbs.models.* 5 | import com.stevdza.san.kotlinbs.models.button.ButtonSize 6 | import com.stevdza.san.kotlinbs.models.button.ButtonVariant 7 | import com.varabyte.kobweb.compose.foundation.layout.Arrangement 8 | import com.varabyte.kobweb.compose.foundation.layout.Box 9 | import com.varabyte.kobweb.compose.foundation.layout.Row 10 | import com.varabyte.kobweb.compose.ui.* 11 | import com.varabyte.kobweb.compose.ui.modifiers.* 12 | import kotlinx.browser.document 13 | import org.jetbrains.compose.web.css.percent 14 | import org.jetbrains.compose.web.css.px 15 | import org.jetbrains.compose.web.dom.Div 16 | import org.w3c.dom.HTMLDivElement 17 | 18 | /** 19 | * Scope which is used to place only Toast components within the [BSToastGroup]. 20 | * */ 21 | class ToastGroupScope 22 | 23 | private val toastGroupScopeImpl = ToastGroupScope() 24 | 25 | /** 26 | * Lightweight and unobtrusive UI element used to display temporary messages or 27 | * notifications to users. It is typically used to show non-disruptive information that 28 | * can be easily dismissed or will automatically disappear after a short period. 29 | * 30 | * This composable represents a group that can contain single or multiple different 31 | * toast components. Call a [showToast] function with specified [BSToast] id in 32 | * order to display it. 33 | * @param placement Where should be this Toast displayed. 34 | * @param content Content that should contain single, or multiple different Toast 35 | * components, that when displayed, should be stacked on top of each other. 36 | * */ 37 | @Composable 38 | fun BSToastGroup( 39 | modifier: Modifier = Modifier, 40 | placement: ToastPlacement = ToastPlacement.BottomRight, 41 | content: @Composable ToastGroupScope.() -> Unit 42 | ) { 43 | Div( 44 | attrs = modifier 45 | .classNames("position-static") 46 | .toAttrs() 47 | ) { 48 | Div( 49 | attrs = Modifier 50 | .padding(all = 16.px) 51 | .classNames(*placement.classes.toTypedArray(), "toast-container") 52 | .styleModifier { 53 | property("pointer-events", "none") 54 | } 55 | .toAttrs() 56 | ) { 57 | content(toastGroupScopeImpl) 58 | } 59 | } 60 | } 61 | 62 | /** 63 | * Lightweight and unobtrusive UI element used to display temporary messages or 64 | * notifications to users. It is typically used to show non-disruptive information that 65 | * can be easily dismissed or will automatically disappear after a short period. 66 | * Call a [showToast] function with specified [BSToast] id in order to display it. 67 | * @param id Unique identifier of the toast component. 68 | * @param title Title of the toast. 69 | * @param body Body of the toast. 70 | * @param autoHide Whether to autohide the toast after a certain delay or not. 71 | * @param indicatorStyle Background color. 72 | * @param onCloseClick Lambda that is triggered when close button is clicked. 73 | * */ 74 | @Composable 75 | fun ToastGroupScope.BSToast( 76 | modifier: Modifier = Modifier, 77 | id: String, 78 | title: String, 79 | body: String, 80 | autoHide: Boolean = true, 81 | indicatorStyle: BackgroundStyle = BackgroundStyle.Primary, 82 | onCloseClick: () -> Unit 83 | ) { 84 | Div( 85 | attrs = modifier 86 | .id(id) 87 | .zIndex(9) 88 | .classNames("toast") 89 | .styleModifier { 90 | property("pointer-events", "auto") 91 | } 92 | .toAttrs { 93 | attr("role", "alert") 94 | attr("aria-live", "assertive") 95 | attr("aria-atomic", "true") 96 | attr("data-bs-autohide", autoHide.toString()) 97 | } 98 | ) { 99 | Row( 100 | modifier = Modifier.classNames("toast-header"), 101 | horizontalArrangement = Arrangement.SpaceBetween 102 | ) { 103 | Row(verticalAlignment = Alignment.CenterVertically) { 104 | Box( 105 | modifier = Modifier 106 | .size(16.px) 107 | .borderRadius(50.percent) 108 | .margin(right = 8.px) 109 | .classNames(indicatorStyle.value) 110 | ) 111 | SpanText(text = title) 112 | } 113 | BSCloseButton( 114 | modifier = Modifier 115 | .onClick { onCloseClick() } 116 | .attrsModifier { 117 | attr("data-bs-dismiss", "toast") 118 | } 119 | ) 120 | } 121 | Div( 122 | attrs = Modifier 123 | .classNames("toast-body") 124 | .toAttrs() 125 | ) { 126 | SpanText(text = body) 127 | } 128 | } 129 | } 130 | 131 | /** 132 | * Toast variant that contains just a text and a close button. 133 | * Lightweight and unobtrusive UI element used to display temporary 134 | * messages or notifications to users. It is typically used to show non-disruptive 135 | * information that can be easily dismissed or will automatically disappear after a short 136 | * period. 137 | * Call a [showToast] function with specified [BSToast] id in order to display it. 138 | * @param id Unique identifier of the toast component. 139 | * @param text Toast text. 140 | * @param style The style of the toast. 141 | * @param autoHide Whether to autohide the toast after a certain delay or not. 142 | * @param closeButtonDark Whether a close icon should have a dark color or not. 143 | * @param onCloseClick Lambda that is triggered when close button is clicked. 144 | * */ 145 | @Composable 146 | fun ToastGroupScope.BSToastBasic( 147 | modifier: Modifier = Modifier, 148 | id: String, 149 | text: String, 150 | style: ToastStyle = ToastStyle.Primary, 151 | autoHide: Boolean = true, 152 | closeButtonDark: Boolean = true, 153 | onCloseClick: () -> Unit 154 | ) { 155 | Div( 156 | attrs = modifier 157 | .id(id) 158 | .zIndex(9) 159 | .classNames("toast", style.value, "border-0") 160 | .styleModifier { 161 | property("pointer-events", "auto") 162 | } 163 | .toAttrs { 164 | attr("role", "alert") 165 | attr("aria-live", "assertive") 166 | attr("aria-atomic", "true") 167 | attr("data-bs-autohide", autoHide.toString()) 168 | } 169 | ) { 170 | Row( 171 | modifier = Modifier.classNames("toast-body"), 172 | horizontalArrangement = Arrangement.SpaceBetween 173 | ) { 174 | SpanText(text = text) 175 | BSCloseButton( 176 | modifier = Modifier 177 | .onClick { onCloseClick() } 178 | .attrsModifier { 179 | attr("data-bs-dismiss", "toast") 180 | }, 181 | dark = closeButtonDark 182 | ) 183 | } 184 | } 185 | } 186 | 187 | /** 188 | * Toast variant that contains two action buttons. 189 | * Lightweight and unobtrusive UI element used to display temporary 190 | * messages or notifications to users. It is typically used to show non-disruptive 191 | * information that can be easily dismissed or will automatically disappear after a short 192 | * period. 193 | * Call a [showToast] function with specified [BSToast] id in order to display it. 194 | * @param id Unique identifier of the toast component. 195 | * @param text Toast text. 196 | * @param positiveButtonText The text of the positive button. 197 | * @param negativeButtonText The text of the negative button. 198 | * @param autoHide Whether to autohide the toast after a certain delay or not. 199 | * @param onPositiveButtonClick A lambda that is triggered when a user clicks a positive 200 | * button. 201 | * @param onNegativeButtonClick A lambda that is triggered when a user clicks a negative 202 | * button. 203 | * @param style The style of the toast. 204 | * @param borderColor The color of the border. 205 | * @param positiveButtonVariant The style of the positive button. 206 | * @param negativeButtonVariant The style of the negative button. 207 | * 208 | * */ 209 | @Composable 210 | fun ToastGroupScope.BSToastAction( 211 | modifier: Modifier = Modifier, 212 | id: String, 213 | text: String, 214 | positiveButtonText: String = "Take Action", 215 | negativeButtonText: String = "Close", 216 | autoHide: Boolean = false, 217 | onPositiveButtonClick: () -> Unit, 218 | onNegativeButtonClick: () -> Unit, 219 | style: ToastStyle = ToastStyle.Link, 220 | borderColor: BorderColor? = null, 221 | positiveButtonVariant: ButtonVariant = ButtonVariant.Primary, 222 | negativeButtonVariant: ButtonVariant = ButtonVariant.Secondary, 223 | ) { 224 | Div( 225 | attrs = modifier 226 | .id(id) 227 | .zIndex(9) 228 | .classNames("toast", style.value) 229 | .styleModifier { 230 | property("pointer-events", "auto") 231 | } 232 | .toAttrs { 233 | attr("role", "alert") 234 | attr("aria-live", "assertive") 235 | attr("aria-atomic", "true") 236 | attr("data-bs-autohide", autoHide.toString()) 237 | } 238 | ) { 239 | Div( 240 | attrs = Modifier.classNames("toast-body").toAttrs(), 241 | ) { 242 | SpanText(text = text) 243 | Row( 244 | modifier = Modifier 245 | .classNames("mt-2", "pt-2", "border-top") 246 | .thenIf( 247 | condition = borderColor != null, 248 | other = Modifier.classNames(borderColor?.value ?: "") 249 | ) 250 | ) { 251 | BSButton( 252 | modifier = Modifier 253 | .attrsModifier { 254 | attr("data-bs-dismiss", "toast") 255 | }, 256 | text = positiveButtonText, 257 | size = ButtonSize.Small, 258 | variant = positiveButtonVariant, 259 | onClick = onPositiveButtonClick 260 | ) 261 | BSButton( 262 | modifier = Modifier 263 | .margin(left = 8.px) 264 | .attrsModifier { 265 | attr("data-bs-dismiss", "toast") 266 | }, 267 | text = negativeButtonText, 268 | size = ButtonSize.Small, 269 | variant = negativeButtonVariant, 270 | onClick = onNegativeButtonClick 271 | ) 272 | } 273 | } 274 | } 275 | } 276 | 277 | /** 278 | * A function which is used to trigger/show a Toast component which you have already 279 | * declared somewhere within your composable hierarchy. 280 | * @param toastId The ID of the Toast component that you've already declared, and want to show. 281 | * */ 282 | fun showToast(toastId: String) { 283 | try { 284 | val toastElement = (document.getElementById(toastId) as HTMLDivElement) 285 | val jsCode = """ 286 | const toast = new bootstrap.Toast(toastElement); 287 | toast.show(); 288 | """.trimIndent() 289 | js("eval(jsCode)") as Unit 290 | } catch (e: Exception) { 291 | println("showToast: ${e.message}") 292 | } 293 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/BSTooltip.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.stevdza.san.kotlinbs.models.TooltipDirection 5 | import com.varabyte.kobweb.compose.ui.Modifier 6 | import com.varabyte.kobweb.compose.ui.toAttrs 7 | import org.jetbrains.compose.web.dom.Div 8 | 9 | /** 10 | * Small pop-up box that provides additional information or context when the user hovers 11 | * over or interacts with an element. Tooltips are commonly used to provide explanations, 12 | * labels, or short descriptions for elements such as buttons, links, or icons. 13 | * [initializeTooltips] function must be called if you want to be able to see those 14 | * same tooltips that you're using. 15 | * @param text Text which will appear inside the tooltip. 16 | * @param direction Direction in which a tooltip will appear. 17 | * @param content Component on top of which you want to add a tooltip. 18 | * */ 19 | @Composable 20 | fun BSTooltip( 21 | text: String, 22 | direction: TooltipDirection = TooltipDirection.Top, 23 | content: @Composable () -> Unit 24 | ) { 25 | Div( 26 | attrs = Modifier 27 | .toAttrs { 28 | attr("data-bs-toggle", "tooltip") 29 | attr("data-bs-placement", direction.value) 30 | attr("data-bs-title", text) 31 | } 32 | ) { 33 | content() 34 | } 35 | } 36 | 37 | /** 38 | * An initialization function which is used with tooltips. If you don't call this function, 39 | * your tooltips will not show. 40 | * */ 41 | fun initializeTooltips() { 42 | try { 43 | val jsCode = """ 44 | var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) 45 | var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { 46 | return new bootstrap.Tooltip(tooltipTriggerEl) 47 | }) 48 | """.trimIndent() 49 | js("eval(jsCode)") as Unit 50 | } catch (e: Exception) { 51 | println("initializeTooltips: ${e.message}") 52 | } 53 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/Offcanvas.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.stevdza.san.kotlinbs.models.offcanvas.OffcanvasPlacement 5 | import com.varabyte.kobweb.compose.ui.Modifier 6 | import com.varabyte.kobweb.compose.ui.attrsModifier 7 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 8 | import com.varabyte.kobweb.compose.ui.modifiers.id 9 | import com.varabyte.kobweb.compose.ui.toAttrs 10 | import org.jetbrains.compose.web.dom.Div 11 | import org.jetbrains.compose.web.dom.H5 12 | 13 | /** 14 | * Offcanvas is used to create sidebar or panel that can slide in and out of the viewport. 15 | * This component is often used to display additional content, navigation menus, 16 | * or options without taking up the entire screen space. 17 | * 18 | * @param id Unique identifier of the component. You will need this in order to 19 | * trigger/show the [BSOffcanvas] component. To do that, specify a [showOffcanvasOnClick] 20 | * modifier on a clickable component, that you want to trigger a [BSOffcanvas] with. 21 | * There's also a [hideOffcanvasOnClick] modifier that is applied on a clickable 22 | * component that you want to use for closing the [BSOffcanvas]. 23 | * @param title The title which is displayed on the top of the component, along with 24 | * a [BSCloseButton]. 25 | * @param body This is where you place a custom components inside the body of this 26 | * component. 27 | * @param dark Whether this component should have a dark background or not. 28 | * @param allowScrolling Whether to allow scrolling on the page, while [BSOffcanvas] 29 | * is active. 30 | * @param disableBackdrop Whether to remove a black overlay behind the component. 31 | * @param closableOutside Whether to allow users to close this component by clicking 32 | * anywhere on the outside. 33 | * @param placement Side on which you want this component to appear. 34 | * */ 35 | @Composable 36 | fun BSOffcanvas( 37 | modifier: Modifier = Modifier, 38 | id: String, 39 | title: String, 40 | body: @Composable () -> Unit, 41 | dark: Boolean = false, 42 | allowScrolling: Boolean = false, 43 | disableBackdrop: Boolean = false, 44 | closableOutside: Boolean = true, 45 | placement: OffcanvasPlacement = OffcanvasPlacement.END 46 | ) { 47 | Div( 48 | attrs = modifier 49 | .id(id) 50 | .classNames("offcanvas",placement.value) 51 | .toAttrs { 52 | attr("tabindex", "-1") 53 | attr("aria-labelledby", "offcanvasLabel") 54 | attr("aria-controls", "#$id") 55 | if (allowScrolling) attr("data-bs-scroll", "true") 56 | if (disableBackdrop) attr("data-bs-backdrop", "false") 57 | if (!closableOutside) attr("data-bs-backdrop", "static") 58 | if (dark) attr("data-bs-theme", "dark") 59 | } 60 | ) { 61 | Div( 62 | attrs = Modifier 63 | .classNames("offcanvas-header") 64 | .toAttrs() 65 | ) { 66 | H5( 67 | attrs = Modifier 68 | .id("offcanvasLabel") 69 | .classNames("offcanvas-title") 70 | .toAttrs() 71 | ) { 72 | SpanText(text = title) 73 | } 74 | BSCloseButton(modifier = Modifier.hideOffcanvasOnClick()) 75 | } 76 | Div( 77 | attrs = Modifier 78 | .classNames("offcanvas-body") 79 | .toAttrs() 80 | ) { 81 | body() 82 | } 83 | } 84 | } 85 | 86 | /** 87 | * Util function which is used to trigger/show [BSOffcanvas] component. 88 | * */ 89 | fun Modifier.showOffcanvasOnClick(id: String): Modifier = attrsModifier { 90 | attr("data-bs-toggle", "offcanvas") 91 | attr("data-bs-target", "#$id") 92 | } 93 | 94 | /** 95 | * Util function which is used to hide [BSOffcanvas] component. 96 | * */ 97 | fun Modifier.hideOffcanvasOnClick(): Modifier = attrsModifier { 98 | attr("data-bs-dismiss", "offcanvas") 99 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/SpanText.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.varabyte.kobweb.compose.ui.Modifier 5 | import com.varabyte.kobweb.compose.ui.toAttrs 6 | import org.jetbrains.compose.web.dom.Span 7 | import org.jetbrains.compose.web.dom.Text 8 | 9 | // This is a loose fork of SpanText from Kobweb's Silk widget set. However, we don't want to depend on Silk, so 10 | // we just duplicate the subset of it we care about ourselves. 11 | 12 | @Composable 13 | internal fun SpanText( 14 | text: String, 15 | modifier: Modifier = Modifier, 16 | ) { 17 | Span(modifier.toAttrs()) { 18 | Text(text) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/components/SvgElement.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.components 2 | 3 | import androidx.compose.runtime.Composable 4 | import kotlinx.browser.document 5 | import org.jetbrains.compose.web.dom.AttrBuilderContext 6 | import org.jetbrains.compose.web.dom.ContentBuilder 7 | import org.jetbrains.compose.web.dom.ElementBuilder 8 | import org.jetbrains.compose.web.dom.TagElement 9 | import org.w3c.dom.Element 10 | import org.w3c.dom.svg.SVGElement 11 | import org.w3c.dom.svg.SVGPathElement 12 | 13 | private val Svg: ElementBuilder = object : ElementBuilder { 14 | private val el: Element by lazy { document.createElementNS("http://www.w3.org/2000/svg", "svg") } 15 | override fun create(): SVGElement = el.cloneNode() as SVGElement 16 | } 17 | 18 | private val Path: ElementBuilder = object : ElementBuilder { 19 | private val el: Element by lazy { document.createElementNS("http://www.w3.org/2000/svg", "path") } 20 | override fun create(): SVGPathElement = el.cloneNode() as SVGPathElement 21 | } 22 | 23 | @Composable 24 | internal fun Svg( 25 | attrs: AttrBuilderContext? = null, 26 | content: ContentBuilder? = null 27 | ) { 28 | TagElement( 29 | elementBuilder = Svg, 30 | applyAttrs = attrs, 31 | content = content 32 | ) 33 | } 34 | 35 | @Composable 36 | internal fun Path( 37 | attrs: AttrBuilderContext? = null, 38 | content: ContentBuilder? = null 39 | ) { 40 | TagElement( 41 | elementBuilder = Path, 42 | applyAttrs = attrs, 43 | content = content 44 | ) 45 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/forms/BSCheckbox.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.forms 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import com.stevdza.san.kotlinbs.models.button.ButtonVariant 6 | import com.stevdza.san.kotlinbs.util.UniqueIdGenerator 7 | import com.varabyte.kobweb.compose.ui.Modifier 8 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 9 | import com.varabyte.kobweb.compose.ui.modifiers.id 10 | import com.varabyte.kobweb.compose.ui.thenIf 11 | import com.varabyte.kobweb.compose.ui.toAttrs 12 | import kotlinx.browser.document 13 | import org.jetbrains.compose.web.attributes.InputType 14 | import org.jetbrains.compose.web.attributes.disabled 15 | import org.jetbrains.compose.web.dom.Div 16 | import org.jetbrains.compose.web.dom.Input 17 | import org.jetbrains.compose.web.dom.Label 18 | import org.jetbrains.compose.web.dom.Text 19 | import org.w3c.dom.HTMLInputElement 20 | 21 | /** 22 | * UI component used for allowing users to make multiple selections from a predefined 23 | * set of options. It represents a checkbox input field with an associated label. 24 | * @param id Unique identifier of the parent. 25 | * @param label Checkbox label. 26 | * @param disabled Whether the checkbox is disabled or not. 27 | * @param defaultChecked Here you specify a default checked state of the checkbox. 28 | * @param reverse Whether you want to display the checkbox and the label in a reverse order. 29 | * @param toggleButton Whether you want to transform the checkbox into a toggle button. 30 | * @param toggleButtonVariant The style of the toggle button. 31 | * @param onClick Lambda that is triggered when a user clicks on a checkbox. 32 | * */ 33 | @Composable 34 | fun BSCheckbox( 35 | modifier: Modifier = Modifier, 36 | id: String? = null, 37 | label: String, 38 | disabled: Boolean = false, 39 | defaultChecked: Boolean = false, 40 | reverse: Boolean = false, 41 | toggleButton: Boolean = false, 42 | toggleButtonVariant: ButtonVariant = ButtonVariant.PrimaryOutline, 43 | onClick: (Boolean) -> Unit 44 | ) { 45 | val randomId = remember { 46 | id ?: UniqueIdGenerator.generateUniqueId("checkbox") 47 | } 48 | Div( 49 | attrs = modifier 50 | .classNames("form-check") 51 | .thenIf( 52 | condition = reverse, 53 | other = Modifier.classNames("form-check-reverse") 54 | ) 55 | .toAttrs() 56 | ) { 57 | Input( 58 | attrs = Modifier 59 | .id(randomId) 60 | .thenIf( 61 | condition = toggleButton, 62 | other = Modifier.classNames("btn-check") 63 | ) 64 | .thenIf( 65 | condition = !toggleButton, 66 | other = Modifier.classNames("form-check-input") 67 | ) 68 | .toAttrs { 69 | if (defaultChecked) defaultChecked() 70 | if (disabled) disabled() 71 | onClick { 72 | onClick((document.getElementById(randomId) as HTMLInputElement).checked) 73 | } 74 | }, 75 | type = InputType.Checkbox 76 | ) 77 | Label( 78 | attrs = Modifier 79 | .thenIf( 80 | condition = toggleButton, 81 | other = Modifier.classNames(*toggleButtonVariant.classes.toTypedArray()) 82 | ) 83 | .thenIf( 84 | condition = !toggleButton, 85 | other = Modifier.classNames("form-check-label") 86 | ) 87 | .toAttrs(), 88 | forId = randomId 89 | ) { 90 | Text(value = label) 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/forms/BSColorPicker.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.forms 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import com.stevdza.san.kotlinbs.models.InputSize 6 | import com.stevdza.san.kotlinbs.util.UniqueIdGenerator 7 | import com.varabyte.kobweb.compose.ui.Modifier 8 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 9 | import com.varabyte.kobweb.compose.ui.modifiers.id 10 | import com.varabyte.kobweb.compose.ui.thenIf 11 | import com.varabyte.kobweb.compose.ui.toAttrs 12 | import org.jetbrains.compose.web.attributes.InputType 13 | import org.jetbrains.compose.web.attributes.disabled 14 | import org.jetbrains.compose.web.dom.Div 15 | import org.jetbrains.compose.web.dom.Input 16 | import org.jetbrains.compose.web.dom.Label 17 | import org.jetbrains.compose.web.dom.Text 18 | 19 | /** 20 | * UI component that allows users to select a color or specify a custom color 21 | * using a color picker. It is used to capture color preferences or to enable users 22 | * to choose colors for various purposes, such as styling, theming, or data visualization. 23 | * @param id Unique identifier of the color picker. 24 | * @param label Color picker label. 25 | * @param title Color picker title. 26 | * @param size Size of a color picker. 27 | * @param disabled Whether this component is disabled or not. 28 | * @param onColorSelected Lambda which is triggered when a user selects a color. 29 | * */ 30 | @Composable 31 | fun BSColorPicker( 32 | modifier: Modifier = Modifier, 33 | id: String? = null, 34 | label: String? = null, 35 | title: String = "Choose your color", 36 | size: InputSize = InputSize.Default, 37 | disabled: Boolean = false, 38 | onColorSelected: (String) -> Unit 39 | ) { 40 | val randomId = remember { 41 | id ?: UniqueIdGenerator.generateUniqueId("colorpicker") 42 | } 43 | Div(attrs = modifier.toAttrs()) { 44 | Label( 45 | attrs = Modifier 46 | .classNames("form-label") 47 | .toAttrs(), 48 | forId = randomId 49 | ) 50 | { 51 | label?.let { Text(value = it) } 52 | } 53 | Input( 54 | attrs = Modifier 55 | .id(randomId) 56 | .classNames("form-control-color", "form-control") 57 | .thenIf( 58 | condition = size != InputSize.Default, 59 | other = Modifier.classNames(size.value) 60 | ) 61 | .toAttrs { 62 | onChange { onColorSelected(it.value) } 63 | if (disabled) disabled() 64 | attr("title", title) 65 | attr("value", "#000000") 66 | }, 67 | type = InputType.Color 68 | ) 69 | } 70 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/forms/BSFileInput.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.forms 2 | 3 | import androidx.compose.runtime.* 4 | import com.stevdza.san.kotlinbs.components.BSButton 5 | import com.stevdza.san.kotlinbs.components.SpanText 6 | import com.stevdza.san.kotlinbs.models.InputSize 7 | import com.stevdza.san.kotlinbs.models.button.ButtonSize 8 | import com.stevdza.san.kotlinbs.models.button.ButtonVariant 9 | import com.stevdza.san.kotlinbs.util.UniqueIdGenerator 10 | import com.varabyte.kobweb.browser.file.loadDataUrlFromDisk 11 | import com.varabyte.kobweb.compose.css.Overflow 12 | import com.varabyte.kobweb.compose.css.TextOverflow 13 | import com.varabyte.kobweb.compose.foundation.layout.Arrangement 14 | import com.varabyte.kobweb.compose.foundation.layout.Row 15 | import com.varabyte.kobweb.compose.ui.Alignment 16 | import com.varabyte.kobweb.compose.ui.Modifier 17 | import com.varabyte.kobweb.compose.ui.graphics.Color 18 | import com.varabyte.kobweb.compose.ui.modifiers.* 19 | import com.varabyte.kobweb.compose.ui.thenIf 20 | import com.varabyte.kobweb.compose.ui.toAttrs 21 | import kotlinx.browser.document 22 | import org.jetbrains.compose.web.css.LineStyle 23 | import org.jetbrains.compose.web.css.cssRem 24 | import org.jetbrains.compose.web.css.px 25 | import org.jetbrains.compose.web.css.rgba 26 | import org.jetbrains.compose.web.dom.Div 27 | import org.jetbrains.compose.web.dom.Label 28 | import org.jetbrains.compose.web.dom.Text 29 | 30 | /** 31 | * UI component that allows users to upload files from their local system 32 | * to a web application. It provides a button and an input field where you can see 33 | * the name of the selected file. 34 | * @param id Unique identifier of the parent. 35 | * @param label File Input label that is shown on top of the component. 36 | * @param placeholder A placeholder text that will show up when a file is not selected. 37 | * @param size The size of the File Input. 38 | * @param disabled Whether this component is disabled or not. 39 | * @param accept A string value that you can use to specify which type is accepted/selectable. 40 | * @param onFileSelected Lambda which is triggered when a user selects a file. The first 41 | * string represents a file name, while the second one represents the actual BASE_64 42 | * encoded file. 43 | * */ 44 | @Composable 45 | fun BSFileInput( 46 | modifier: Modifier = Modifier, 47 | id: String? = null, 48 | label: String? = null, 49 | placeholder: String = "No file selected.", 50 | size: InputSize = InputSize.Default, 51 | disabled: Boolean = false, 52 | accept: String = "image/png, image/jpeg", 53 | onFileSelected: (String, String) -> Unit 54 | ) { 55 | val randomId = remember { 56 | id ?: UniqueIdGenerator.generateUniqueId("fileinput") 57 | } 58 | var placeholderText by remember { mutableStateOf(placeholder) } 59 | Div(attrs = modifier.toAttrs()) { 60 | if(label != null) { 61 | Label( 62 | attrs = Modifier 63 | .classNames("form-label") 64 | .toAttrs(), 65 | forId = randomId 66 | ) 67 | { 68 | Text(value = label) 69 | } 70 | } 71 | Row( 72 | modifier = Modifier 73 | .id(randomId) 74 | .thenIf( 75 | condition = disabled, 76 | // TODO: Will this color get used anywhere? Should we extract it into a constant? 77 | other = Modifier.backgroundColor(Color.rgb(0xFAFAFA)) 78 | ) 79 | .border( 80 | width = 1.px, 81 | style = LineStyle.Solid, 82 | color = rgba(r = 206, g = 212, b = 218, a = 1) 83 | ) 84 | .padding(all = 0.px) 85 | .onClick { 86 | if (!disabled) { 87 | document.loadDataUrlFromDisk( 88 | accept = accept, 89 | onLoad = { 90 | onFileSelected(filename, it) 91 | placeholderText = filename 92 | } 93 | ) 94 | } 95 | } 96 | .overflow(Overflow.Hidden) 97 | .textOverflow(TextOverflow.Ellipsis) 98 | .thenIf( 99 | condition = size != InputSize.Default, 100 | other = Modifier.classNames(size.value) 101 | ) 102 | .borderRadius(0.375.cssRem), 103 | verticalAlignment = Alignment.CenterVertically, 104 | horizontalArrangement = Arrangement.Start 105 | ) { 106 | BSButton( 107 | modifier = Modifier 108 | .margin(all = 0.px) 109 | .borderRadius(topRight = 0.px, bottomRight = 0.px), 110 | text = "Browse...", 111 | variant = ButtonVariant.Light, 112 | size = when (size) { 113 | InputSize.Default -> { 114 | ButtonSize.Default 115 | } 116 | 117 | InputSize.Small -> { 118 | ButtonSize.Small 119 | } 120 | 121 | InputSize.Large -> { 122 | ButtonSize.Large 123 | } 124 | }, 125 | disabled = disabled, 126 | onClick = {} 127 | ) 128 | SpanText( 129 | modifier = Modifier 130 | .thenIf( 131 | condition = disabled, 132 | other = Modifier.classNames("text-muted") 133 | ) 134 | .margin(leftRight = 12.px), 135 | text = placeholderText 136 | ) 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/forms/BSInput.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.forms 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import com.stevdza.san.kotlinbs.models.InputSize 6 | import com.stevdza.san.kotlinbs.models.InputValidation 7 | import com.stevdza.san.kotlinbs.util.UniqueIdGenerator 8 | import com.varabyte.kobweb.compose.ui.Modifier 9 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 10 | import com.varabyte.kobweb.compose.ui.modifiers.id 11 | import com.varabyte.kobweb.compose.ui.thenIf 12 | import com.varabyte.kobweb.compose.ui.toAttrs 13 | import org.jetbrains.compose.web.attributes.InputType 14 | import org.jetbrains.compose.web.attributes.disabled 15 | import org.jetbrains.compose.web.attributes.readOnly 16 | import org.jetbrains.compose.web.attributes.required 17 | import org.jetbrains.compose.web.dom.Div 18 | import org.jetbrains.compose.web.dom.Input 19 | import org.jetbrains.compose.web.dom.Label 20 | import org.jetbrains.compose.web.dom.Text 21 | 22 | /** 23 | * UI component used to capture user input or display information. It provides a text field 24 | * or other types of input fields where users can enter data. 25 | * @param id Unique identifier of the component. 26 | * @param label Input field label. 27 | * @param placeholder Input field placeholder. 28 | * @param type The type of the input. 29 | * @param size The size of the input. 30 | * @param disabled Whether this component should be disabled or not. 31 | * @param readOnly Whether to enable a read-only state of the component. 32 | * @param plainText Whether to remove all default styling from the component. 33 | * @param floating Whether to use a floating variant of the component. 34 | * @param required This field is particularly used inside a Form to indicate that 35 | * the form cannot be submitted if this field is empty. 36 | * @param validation Here you can customize the look of the input component, in two 37 | * different states: Success and Error. 38 | * @param value A default value that will be included inside the input. 39 | * @param onValueChange Lambda that will be triggered when a user types something. 40 | * @param onEnterClick Lambda that triggers when you press 'Enter' key on your keyword, 41 | * while the input component is focused. 42 | * inside the component. 43 | * */ 44 | @Composable 45 | fun BSInput( 46 | modifier: Modifier = Modifier, 47 | id: String? = null, 48 | label: String? = null, 49 | placeholder: String? = null, 50 | type: InputType = InputType.Text, 51 | size: InputSize = InputSize.Default, 52 | disabled: Boolean = false, 53 | readOnly: Boolean = false, 54 | plainText: Boolean = false, 55 | floating: Boolean = false, 56 | required: Boolean = false, 57 | validation: InputValidation = InputValidation(), 58 | value: String, 59 | onValueChange: (String) -> Unit, 60 | onEnterClick: (() -> Unit)? = null 61 | ) { 62 | val randomId = remember { 63 | id ?: UniqueIdGenerator.generateUniqueId("input") 64 | } 65 | Div( 66 | attrs = modifier 67 | .thenIf( 68 | condition = floating, 69 | other = Modifier.classNames("form-floating") 70 | ) 71 | .toAttrs() 72 | ) { 73 | if (!floating && label != null) { 74 | Label( 75 | attrs = Modifier 76 | .classNames("form-label") 77 | .toAttrs(), 78 | forId = randomId 79 | ) 80 | { 81 | Text(value = label) 82 | } 83 | } 84 | Input( 85 | attrs = Modifier 86 | .id(randomId) 87 | .classNames(if (plainText) "form-control-plaintext" else "form-control") 88 | .thenIf( 89 | condition = validation.isValid, 90 | other = Modifier.classNames("is-valid") 91 | ) 92 | .thenIf( 93 | condition = validation.isInvalid, 94 | other = Modifier.classNames("is-invalid") 95 | ) 96 | .thenIf( 97 | condition = size != InputSize.Default, 98 | other = Modifier.classNames(size.value) 99 | ) 100 | .toAttrs { 101 | value(value) 102 | onInput { onValueChange(it.value) } 103 | onKeyUp { 104 | if (onEnterClick != null) { 105 | it.preventDefault() 106 | if (it.key == "Enter") { 107 | onEnterClick() 108 | } 109 | } 110 | } 111 | if (!placeholder.isNullOrEmpty()) attr("placeholder", placeholder) 112 | if (disabled) disabled() 113 | if (readOnly) readOnly() 114 | if (required) required() 115 | }, 116 | type = type 117 | ) 118 | if (validation.isValid) { 119 | Div( 120 | attrs = Modifier 121 | .classNames("valid-feedback") 122 | .toAttrs() 123 | ) { 124 | Text(value = validation.validFeedback) 125 | } 126 | } 127 | if (validation.isInvalid) { 128 | Div( 129 | attrs = Modifier 130 | .classNames("invalid-feedback") 131 | .toAttrs() 132 | ) { 133 | Text(value = validation.invalidFeedback) 134 | } 135 | } 136 | if (floating && label != null) { 137 | Label( 138 | attrs = Modifier 139 | .classNames("form-label") 140 | .toAttrs(), 141 | forId = randomId 142 | ) 143 | { 144 | Text(value = label) 145 | } 146 | } 147 | } 148 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/forms/BSRadioButton.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.forms 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.CompositionLocalProvider 5 | import androidx.compose.runtime.compositionLocalOf 6 | import androidx.compose.runtime.remember 7 | import com.stevdza.san.kotlinbs.models.button.ButtonVariant 8 | import com.stevdza.san.kotlinbs.util.UniqueIdGenerator 9 | import com.varabyte.kobweb.compose.ui.Modifier 10 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 11 | import com.varabyte.kobweb.compose.ui.modifiers.id 12 | import com.varabyte.kobweb.compose.ui.thenIf 13 | import com.varabyte.kobweb.compose.ui.toAttrs 14 | import org.jetbrains.compose.web.attributes.AutoComplete 15 | import org.jetbrains.compose.web.attributes.InputType 16 | import org.jetbrains.compose.web.attributes.autoComplete 17 | import org.jetbrains.compose.web.attributes.disabled 18 | import org.jetbrains.compose.web.dom.Div 19 | import org.jetbrains.compose.web.dom.Input 20 | import org.jetbrains.compose.web.dom.Label 21 | import org.jetbrains.compose.web.dom.Text 22 | 23 | private val radioGroupScopeImpl = BSRadioGroupScope() 24 | 25 | /** 26 | * Component used for allowing users to make a single selection from a predefined 27 | * set of options. This component is used within the [BSRadioButtonGroup]. 28 | * @param id Unique identifier of the component. 29 | * @param label Label of the radio button. 30 | * @param disabled Whether this component should be disabled or not. 31 | * @param onClick Lambda that is triggered when a user makes a selection. 32 | * */ 33 | @Composable 34 | fun BSRadioGroupScope.BSRadioButton( 35 | modifier: Modifier = Modifier, 36 | id: String? = null, 37 | label: String, 38 | disabled: Boolean = false, 39 | onClick: () -> Unit 40 | ) { 41 | val name = getCompositionLocalGroupName() 42 | val checkedValue = getCompositionLocalCheckedValue() 43 | val inline = getCompositionLocalInlineValue() 44 | val reverse = getCompositionLocalReverseValue() 45 | val toggleButton = getCompositionLocalToggleButton() 46 | val toggleButtonStyle = getCompositionLocalToggleButtonStyle() 47 | val randomId = remember { 48 | id ?: UniqueIdGenerator.generateUniqueId("radiobutton") 49 | } 50 | Div( 51 | attrs = modifier 52 | .classNames("form-check") 53 | .thenIf( 54 | condition = inline, 55 | other = Modifier.classNames("form-check-inline") 56 | ) 57 | .thenIf( 58 | condition = reverse, 59 | other = Modifier.classNames("form-check-reverse") 60 | ) 61 | .toAttrs() 62 | ) { 63 | Input( 64 | attrs = Modifier 65 | .id(randomId) 66 | .thenIf( 67 | condition = toggleButton, 68 | other = Modifier.classNames("btn-check") 69 | ) 70 | .thenIf( 71 | condition = !toggleButton, 72 | other = Modifier.classNames("form-check-input") 73 | ) 74 | .toAttrs { 75 | if (checkedValue == label) defaultChecked() 76 | if (disabled) disabled() 77 | if (toggleButton) autoComplete(AutoComplete.off) 78 | attr("name", name) 79 | onClick { onClick() } 80 | }, 81 | type = InputType.Radio 82 | ) 83 | Label( 84 | attrs = Modifier 85 | .thenIf( 86 | condition = toggleButton, 87 | other = Modifier.classNames(*toggleButtonStyle.toTypedArray()) 88 | ) 89 | .thenIf( 90 | condition = !toggleButton, 91 | other = Modifier.classNames("form-check-label") 92 | ) 93 | .toAttrs(), 94 | forId = randomId 95 | ) { 96 | Text(value = label) 97 | } 98 | } 99 | } 100 | 101 | /** 102 | * Component used for allowing users to make a single selection from a predefined 103 | * * set of options. This component is used to wrap multiple different [BSRadioButton]'s 104 | * where only a single selection is allowed. 105 | * @param name The name of this group (It should be unique). 106 | * @param checkedValue A string value that represents a radio button that should be 107 | * selected by default. This value needs to be the same as the name of the [BSRadioButton]'s 108 | * label. 109 | * @param inline Whether this group should be inlined, instead of placed within a column. 110 | * @param reverse Whether to reverse the order of the radio button and a label. 111 | * @param toggleButton Whether to change the style of the radio buttons to toggle buttons. 112 | * @param toggleButtonVariant The style of the toggle button. 113 | * @param content Here, inside the lambda we can call one or multiple [BSRadioButton]'s. 114 | * */ 115 | @Composable 116 | fun BSRadioButtonGroup( 117 | modifier: Modifier = Modifier, 118 | name: String? = null, 119 | checkedValue: String? = null, 120 | inline: Boolean = false, 121 | reverse: Boolean = false, 122 | toggleButton: Boolean = false, 123 | toggleButtonVariant: ButtonVariant = ButtonVariant.PrimaryOutline, 124 | content: @Composable BSRadioGroupScope.() -> Unit 125 | ) { 126 | val radioGroupName = remember { name ?: radioGroupScopeImpl.generateNextRadioGroupName() } 127 | 128 | CompositionLocalProvider( 129 | radioGroupScopeImpl.checkedValueCompositionLocal provides checkedValue, 130 | radioGroupScopeImpl.groupNameCompositionLocal provides radioGroupName, 131 | radioGroupScopeImpl.inlineCompositionLocal provides inline, 132 | radioGroupScopeImpl.reverseCompositionLocal provides reverse, 133 | radioGroupScopeImpl.toggleButtonCompositionLocal provides toggleButton, 134 | radioGroupScopeImpl.toggleButtonStyleCompositionLocal provides toggleButtonVariant.classes, 135 | content = { 136 | Div(attrs = modifier.toAttrs()) { 137 | content(radioGroupScopeImpl) 138 | } 139 | } 140 | ) 141 | } 142 | 143 | class BSRadioGroupScope { 144 | private var radioGroupNamesCounter = 0 145 | 146 | internal val checkedValueCompositionLocal = compositionLocalOf { 147 | error("No radio group checked value provided") 148 | } 149 | 150 | internal val groupNameCompositionLocal = compositionLocalOf { 151 | error("No radio group name provided") 152 | } 153 | 154 | internal val inlineCompositionLocal = compositionLocalOf { 155 | error("No inline value provided") 156 | } 157 | 158 | internal val reverseCompositionLocal = compositionLocalOf { 159 | error("No reverse value provided") 160 | } 161 | 162 | internal val toggleButtonCompositionLocal = compositionLocalOf { 163 | error("No toggle button provided") 164 | } 165 | 166 | internal val toggleButtonStyleCompositionLocal = compositionLocalOf> { 167 | error("No toggle button style provided") 168 | } 169 | 170 | internal fun generateNextRadioGroupName(): String { 171 | return "\$compose\$generated\$radio\$group-${radioGroupNamesCounter++}" 172 | } 173 | } 174 | 175 | @Composable 176 | internal fun BSRadioGroupScope.getCompositionLocalCheckedValue(): String? { 177 | return checkedValueCompositionLocal.current 178 | } 179 | 180 | @Composable 181 | internal fun BSRadioGroupScope.getCompositionLocalGroupName(): String { 182 | return groupNameCompositionLocal.current 183 | } 184 | 185 | @Composable 186 | internal fun BSRadioGroupScope.getCompositionLocalInlineValue(): Boolean { 187 | return inlineCompositionLocal.current 188 | } 189 | 190 | @Composable 191 | internal fun BSRadioGroupScope.getCompositionLocalReverseValue(): Boolean { 192 | return reverseCompositionLocal.current 193 | } 194 | 195 | @Composable 196 | internal fun BSRadioGroupScope.getCompositionLocalToggleButton(): Boolean { 197 | return toggleButtonCompositionLocal.current 198 | } 199 | 200 | @Composable 201 | internal fun BSRadioGroupScope.getCompositionLocalToggleButtonStyle(): List { 202 | return toggleButtonStyleCompositionLocal.current 203 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/forms/BSRange.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.forms 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import com.stevdza.san.kotlinbs.util.UniqueIdGenerator 6 | import com.varabyte.kobweb.compose.ui.Modifier 7 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 8 | import com.varabyte.kobweb.compose.ui.modifiers.id 9 | import com.varabyte.kobweb.compose.ui.toAttrs 10 | import kotlinx.browser.document 11 | import org.jetbrains.compose.web.attributes.* 12 | import org.jetbrains.compose.web.dom.Div 13 | import org.jetbrains.compose.web.dom.Input 14 | import org.jetbrains.compose.web.dom.Label 15 | import org.jetbrains.compose.web.dom.Text 16 | import org.w3c.dom.HTMLInputElement 17 | 18 | /** 19 | * This UI component is used to capture a numeric value within a specified range. 20 | * It provides a slider control that users can interact with, to select a value within 21 | * the defined range. 22 | * @param id Unique identifier of the component. 23 | * @param label Label of this component. 24 | * @param min The minimum number which is used in this range. 25 | * @param max The maximum number which is used in this range. 26 | * @param step A double value that represents the steps that will be counted while 27 | * interacting with a component. 28 | * @param disabled Whether this component should be disabled or not. 29 | * @param onSelect Lambda which is triggered when you select a value on a slider. 30 | * It provides a double value that represents a selected number from that range. 31 | * */ 32 | @Composable 33 | fun BSRange( 34 | modifier: Modifier = Modifier, 35 | id: String? = null, 36 | label: String? = null, 37 | min: Int = 0, 38 | max: Int = 100, 39 | step: Double = 1.0, 40 | disabled: Boolean = false, 41 | onSelect: (Double) -> Unit 42 | ) { 43 | val randomId = remember { 44 | id ?: UniqueIdGenerator.generateUniqueId("range") 45 | } 46 | Div(attrs = modifier.toAttrs()) { 47 | if (label != null) { 48 | Label( 49 | attrs = Modifier 50 | .classNames("form-label") 51 | .toAttrs(), 52 | forId = randomId 53 | ) { 54 | Text(value = label) 55 | } 56 | } 57 | Input( 58 | attrs = Modifier 59 | .id(randomId) 60 | .classNames("form-range") 61 | .toAttrs { 62 | if (disabled) disabled() 63 | min(value = "$min") 64 | max(value = "$max") 65 | step(value = step) 66 | onChange { 67 | onSelect((document.getElementById(randomId) as HTMLInputElement).value.toDouble()) 68 | } 69 | }, 70 | type = InputType.Range 71 | ) 72 | } 73 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/forms/BSSelect.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.forms 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import com.stevdza.san.kotlinbs.models.InputValidation 6 | import com.stevdza.san.kotlinbs.models.SelectSize 7 | import com.stevdza.san.kotlinbs.util.UniqueIdGenerator 8 | import com.varabyte.kobweb.compose.css.disabled 9 | import com.varabyte.kobweb.compose.ui.Modifier 10 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 11 | import com.varabyte.kobweb.compose.ui.modifiers.id 12 | import com.varabyte.kobweb.compose.ui.thenIf 13 | import com.varabyte.kobweb.compose.ui.toAttrs 14 | import org.jetbrains.compose.web.attributes.selected 15 | import org.jetbrains.compose.web.dom.* 16 | 17 | /** 18 | * This component allows you to make a single selection from a list of predefined 19 | * options. Select components are widely used for various purposes, such as selecting 20 | * a country in a registration form, choosing a product category in an e-commerce site, etc. 21 | * @param id A unique identifier of the component. 22 | * @param items Here you define a list of items to be displayed inside this component. 23 | * @param placeholder Placeholder that that will be displayed by default. 24 | * @param size The size of the component. 25 | * @param validation Define the Valid/Invalid style of the component. 26 | * @param disabled Whether this component should be disabled or not. 27 | * @param floating Whether to use the floating style of the component. 28 | * @param floatingLabel The label that will be displayed on top of the component if 29 | * the [floating] is enabled. 30 | * @param onItemSelect Lambda which is triggered when you select an option from this 31 | * component. It provides an index value and the text of the selected option. 32 | * */ 33 | @Composable 34 | fun BSSelect( 35 | modifier: Modifier = Modifier, 36 | id: String? = null, 37 | items: List, 38 | placeholder: String? = null, 39 | size: SelectSize = SelectSize.Default, 40 | validation: InputValidation = InputValidation(), 41 | disabled: Boolean = false, 42 | floating: Boolean = false, 43 | floatingLabel: String = "Select", 44 | onItemSelect: (Int, String) -> Unit 45 | ) { 46 | val randomId = remember { 47 | id ?: UniqueIdGenerator.generateUniqueId("select") 48 | } 49 | if (floating) { 50 | Div( 51 | attrs = modifier 52 | .classNames("form-floating") 53 | .toAttrs() 54 | ) { 55 | BSSelectInternal( 56 | id = randomId, 57 | items = items, 58 | placeholder = placeholder, 59 | validation = validation, 60 | size = size, 61 | disabled = disabled, 62 | onItemSelect = onItemSelect 63 | ) 64 | Label( 65 | attrs = Modifier 66 | .classNames("form-label") 67 | .toAttrs(), 68 | forId = randomId 69 | ) { 70 | Text(value = floatingLabel) 71 | } 72 | } 73 | } else { 74 | Div(attrs = modifier.toAttrs()) { 75 | BSSelectInternal( 76 | modifier = modifier, 77 | id = randomId, 78 | items = items, 79 | placeholder = placeholder, 80 | validation = validation, 81 | size = size, 82 | disabled = disabled, 83 | onItemSelect = onItemSelect 84 | ) 85 | } 86 | } 87 | } 88 | 89 | @Composable 90 | private fun BSSelectInternal( 91 | modifier: Modifier? = null, 92 | id: String, 93 | items: List, 94 | placeholder: String?, 95 | validation: InputValidation, 96 | size: SelectSize, 97 | disabled: Boolean, 98 | onItemSelect: (Int, String) -> Unit 99 | ) { 100 | Select( 101 | attrs = Modifier 102 | .then(modifier ?: Modifier) 103 | .id(id) 104 | .classNames("form-select") 105 | .thenIf( 106 | condition = validation.isValid, 107 | other = Modifier.classNames("is-valid") 108 | ) 109 | .thenIf( 110 | condition = validation.isInvalid, 111 | other = Modifier.classNames("is-invalid") 112 | ) 113 | .thenIf( 114 | condition = size != SelectSize.Default, 115 | other = Modifier.classNames(size.value) 116 | ) 117 | .toAttrs { 118 | if (disabled) disabled() 119 | onChange { 120 | it.value?.let { text -> 121 | if (text != placeholder) { 122 | onItemSelect(items.indexOf(text), text) 123 | } 124 | } 125 | } 126 | } 127 | ) { 128 | if (!placeholder.isNullOrEmpty()) { 129 | Option( 130 | attrs = Modifier.toAttrs { selected() }, 131 | value = placeholder 132 | ) { 133 | Text(placeholder) 134 | } 135 | } 136 | items.forEach { text -> 137 | Option(value = text) { 138 | Text(value = text) 139 | } 140 | } 141 | } 142 | if (validation.isValid) { 143 | Div( 144 | attrs = Modifier 145 | .classNames("valid-feedback") 146 | .toAttrs() 147 | ) { 148 | Text(value = validation.validFeedback) 149 | } 150 | } 151 | if (validation.isInvalid) { 152 | Div( 153 | attrs = Modifier 154 | .classNames("invalid-feedback") 155 | .toAttrs() 156 | ) { 157 | Text(value = validation.invalidFeedback) 158 | } 159 | } 160 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/forms/BSSwitch.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.forms 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import com.stevdza.san.kotlinbs.util.UniqueIdGenerator 6 | import com.varabyte.kobweb.compose.ui.Modifier 7 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 8 | import com.varabyte.kobweb.compose.ui.modifiers.id 9 | import com.varabyte.kobweb.compose.ui.toAttrs 10 | import kotlinx.browser.document 11 | import org.jetbrains.compose.web.attributes.InputType 12 | import org.jetbrains.compose.web.attributes.disabled 13 | import org.jetbrains.compose.web.dom.Div 14 | import org.jetbrains.compose.web.dom.Input 15 | import org.jetbrains.compose.web.dom.Label 16 | import org.jetbrains.compose.web.dom.Text 17 | import org.w3c.dom.HTMLInputElement 18 | 19 | /** 20 | * UI component used for binary on/off or true/false selections. It provides a visually 21 | * appealing toggle button that can be toggled between two states. 22 | * @param id Unique identifier of the component. 23 | * @param label The label of this component. 24 | * @param checked Whether this switch is checked or not. 25 | * @param disabled Whether this switch should be disabled or not. 26 | * @param onClick Lambda that is triggered when a user clicks the switch. It holds the 27 | * boolean value that represents a checked state. 28 | * */ 29 | @Composable 30 | fun BSSwitch( 31 | modifier: Modifier = Modifier, 32 | id: String? = null, 33 | label: String, 34 | checked: Boolean = false, 35 | disabled: Boolean = false, 36 | onClick: (Boolean) -> Unit 37 | ) { 38 | val randomId = remember { 39 | id ?: UniqueIdGenerator.generateUniqueId("switch") 40 | } 41 | Div( 42 | attrs = modifier 43 | .classNames("form-check", "form-switch") 44 | .toAttrs() 45 | ) { 46 | Input( 47 | attrs = Modifier 48 | .id(randomId) 49 | .classNames("form-check-input") 50 | .toAttrs { 51 | attr("role", "switch") 52 | this@toAttrs.checked(checked) 53 | if (disabled) disabled() 54 | onClick { 55 | onClick((document.getElementById(randomId) as HTMLInputElement).checked) 56 | } 57 | }, 58 | type = InputType.Checkbox 59 | ) 60 | Label( 61 | attrs = Modifier 62 | .classNames("form-check-label") 63 | .toAttrs(), 64 | forId = randomId 65 | ) { 66 | Text(value = label) 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/forms/BSTextArea.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.forms 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import com.stevdza.san.kotlinbs.models.InputSize 6 | import com.stevdza.san.kotlinbs.models.InputValidation 7 | import com.stevdza.san.kotlinbs.util.UniqueIdGenerator 8 | import com.varabyte.kobweb.compose.ui.Modifier 9 | import com.varabyte.kobweb.compose.ui.modifiers.classNames 10 | import com.varabyte.kobweb.compose.ui.modifiers.id 11 | import com.varabyte.kobweb.compose.ui.thenIf 12 | import com.varabyte.kobweb.compose.ui.toAttrs 13 | import org.jetbrains.compose.web.attributes.disabled 14 | import org.jetbrains.compose.web.attributes.readOnly 15 | import org.jetbrains.compose.web.attributes.required 16 | import org.jetbrains.compose.web.dom.Div 17 | import org.jetbrains.compose.web.dom.Label 18 | import org.jetbrains.compose.web.dom.Text 19 | import org.jetbrains.compose.web.dom.TextArea 20 | 21 | /** 22 | * UI component used to capture multi-line text input from users. It provides a resizable 23 | * input field that allows users to enter and edit longer blocks of text. 24 | * @param id Unique identifier of the component 25 | * @param value A default value that will be included inside the input. 26 | * @param label Label of the component, 27 | * @param placeholder Placeholder of the component. 28 | * @param disabled Whether this component is disabled or not. 29 | * @param readOnly Whether to enable a read-only state of the component. 30 | * @param floating Whether to use a floating variant of the component. 31 | * @param required This field is particularly used inside a Form to indicate that 32 | * @param size The size of the TextArea component. 33 | * @param validation Here you can customize the look of the input component, in two 34 | * different states: Success and Error. 35 | * @param onValueChange Lambda that will be triggered when a user types something 36 | * inside the component. 37 | * */ 38 | @Composable 39 | fun BSTextArea( 40 | modifier: Modifier = Modifier, 41 | id: String? = null, 42 | value: String, 43 | label: String? = null, 44 | placeholder: String? = null, 45 | disabled: Boolean = false, 46 | readOnly: Boolean = false, 47 | floating: Boolean = false, 48 | required: Boolean = false, 49 | size: InputSize = InputSize.Default, 50 | validation: InputValidation = InputValidation(), 51 | onValueChange: (String) -> Unit 52 | ) { 53 | val randomId = remember { 54 | id ?: UniqueIdGenerator.generateUniqueId("textarea") 55 | } 56 | Div( 57 | attrs = modifier 58 | .thenIf( 59 | condition = floating, 60 | other = Modifier.classNames("form-floating") 61 | ) 62 | .toAttrs() 63 | ) { 64 | if (!floating && label != null) { 65 | Label( 66 | attrs = Modifier 67 | .classNames("form-label") 68 | .toAttrs(), 69 | forId = randomId 70 | ) 71 | { 72 | Text(value = label) 73 | } 74 | } 75 | TextArea( 76 | attrs = modifier 77 | .id(randomId) 78 | .thenIf( 79 | condition = size != InputSize.Default, 80 | other = Modifier.classNames(size.value) 81 | ) 82 | .thenIf( 83 | condition = validation.isValid, 84 | other = Modifier.classNames("is-valid") 85 | ) 86 | .thenIf( 87 | condition = validation.isInvalid, 88 | other = Modifier.classNames("is-invalid") 89 | ) 90 | .classNames("form-control") 91 | .toAttrs { 92 | value(value) 93 | onInput { onValueChange(it.value) } 94 | if (!placeholder.isNullOrEmpty()) attr("placeholder", placeholder) 95 | if (readOnly) readOnly() 96 | if (disabled) disabled() 97 | if (required) required() 98 | } 99 | ) 100 | if (validation.isValid) { 101 | Div( 102 | attrs = Modifier 103 | .classNames("valid-feedback") 104 | .toAttrs() 105 | ) { 106 | Text(value = validation.validFeedback) 107 | } 108 | } 109 | if (validation.isInvalid) { 110 | Div( 111 | attrs = Modifier 112 | .classNames("invalid-feedback") 113 | .toAttrs() 114 | ) { 115 | Text(value = validation.invalidFeedback) 116 | } 117 | } 118 | if (floating && label != null) { 119 | Label(forId = randomId) { 120 | Text(value = label) 121 | } 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/AccordionItem.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | data class AccordionItem( 6 | val title: String, 7 | val content: @Composable () -> Unit, 8 | val defaultOpened: Boolean = false 9 | ) -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/AlertIcon.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | enum class AlertIcon( 4 | val classes: String, 5 | val label: String, 6 | val path: String 7 | ) { 8 | Info( 9 | classes = "bi-info-fill", 10 | label = "Info:", 11 | path = "M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2z" 12 | ), 13 | Warning( 14 | classes = "bi-exclamation-triangle-fill", 15 | label = "Warning:", 16 | path = "M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" 17 | ), 18 | Checkmark( 19 | classes = "bi-check-circle-fill", 20 | label = "Success:", 21 | path = "M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z" 22 | ) 23 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/AlertStyle.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | enum class AlertStyle(val classes: List) { 4 | Primary(classes = listOf("alert", "alert-primary")), 5 | Secondary(classes = listOf("alert", "alert-secondary")), 6 | Success(classes = listOf("alert", "alert-success")), 7 | Danger(classes = listOf("alert", "alert-danger")), 8 | Warning(classes = listOf("alert", "alert-warning")), 9 | Info(classes = listOf("alert", "alert-info")), 10 | Light(classes = listOf("alert", "alert-light")), 11 | Dark(classes = listOf("alert", "alert-dark")) 12 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/BSBorderRadius.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | import com.varabyte.kobweb.compose.css.CSSLengthOrPercentageNumericValue 4 | import org.jetbrains.compose.web.css.px 5 | 6 | data class BSBorderRadius( 7 | val topLeft: CSSLengthOrPercentageNumericValue = 0.px, 8 | val topRight: CSSLengthOrPercentageNumericValue = 0.px, 9 | val bottomRight: CSSLengthOrPercentageNumericValue = 0.px, 10 | val bottomLeft: CSSLengthOrPercentageNumericValue = 0.px 11 | ) { 12 | constructor(all: CSSLengthOrPercentageNumericValue = 0.px) : this( 13 | topLeft = all, 14 | topRight = all, 15 | bottomRight = all, 16 | bottomLeft = all 17 | ) 18 | 19 | val value = "$topLeft $topRight $bottomRight $bottomLeft" 20 | } 21 | -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/BackgroundStyle.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | enum class BackgroundStyle(val value: String) { 4 | Primary(value = "bg-primary"), 5 | Secondary(value = "bg-secondary"), 6 | Success(value = "bg-success"), 7 | Danger(value = "bg-danger"), 8 | Warning(value = "bg-warning"), 9 | Info(value = "bg-info"), 10 | Light(value = "bg-light"), 11 | Dark(value = "bg-dark") 12 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/BadgeVariant.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | enum class BadgeVariant(val classes: List? = null) { 4 | Regular, 5 | Straight, 6 | Rounded(listOf("rounded-pill")), 7 | Empty(listOf("border", "border-light", "rounded-circle", "p-2")) 8 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/BorderColor.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | enum class BorderColor(val value: String) { 4 | Primary(value = "border-primary"), 5 | Secondary(value = "border-secondary"), 6 | Success(value = "border-success"), 7 | Danger(value = "border-danger"), 8 | Warning(value = "border-warning"), 9 | Info(value = "border-info"), 10 | Dark(value = "border-dark"), 11 | Light(value = "border-light"), 12 | White(value = "border-white") 13 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/BreadcrumbItem.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | data class BreadcrumbItem( 4 | val text: String, 5 | val href: String 6 | ) 7 | -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/CarouselItem.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | data class CarouselItem( 4 | val image: String, 5 | val title: String? = null, 6 | val body: String? = null 7 | ) 8 | -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/DropdownDirection.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | enum class DropdownDirection(val value: String? = null) { 4 | Bottom, 5 | Top(value = "dropup"), 6 | Left(value = "dropstart"), 7 | Right(value = "dropend") 8 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/InputSize.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | enum class InputSize(val value: String) { 4 | Default(value = ""), 5 | Small(value = "form-control-sm"), 6 | Large(value = "form-control-lg"), 7 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/InputValidation.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | data class InputValidation( 4 | val isValid: Boolean = false, 5 | val isInvalid: Boolean = false, 6 | val validFeedback: String = "Correct!", 7 | val invalidFeedback: String = "Incorrect!" 8 | ) 9 | -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/ModalSize.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | enum class ModalSize(val value: String) { 4 | None(value = ""), 5 | Small(value = "modal-sm"), 6 | Large(value = "modal-lg"), 7 | ExtraLarge(value = "modal-xl") 8 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/PaginationButtons.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | data class PreviousButton( 4 | val text: String = "Previous", 5 | val disabled: Boolean = false, 6 | val onClick: (Int) -> Unit 7 | ) 8 | 9 | data class NextButton( 10 | val text: String = "Next", 11 | val disabled: Boolean = false, 12 | val onClick: (Int) -> Unit 13 | ) -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/PaginationSize.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | enum class PaginationSize(val classes: List) { 4 | Default(classes = listOf("pagination")), 5 | Small(classes = listOf("pagination", "pagination-sm")), 6 | Large(classes = listOf("pagination", "pagination-lg")) 7 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/SelectSize.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | enum class SelectSize(val value: String) { 4 | Default(value = ""), 5 | Small(value = "form-select-sm"), 6 | Large(value = "form-select-lg"), 7 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/SpinnerStyle.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | enum class SpinnerStyle(val value: String) { 4 | Primary(value = "text-primary"), 5 | Secondary(value = "text-secondary"), 6 | Success(value = "text-success"), 7 | Danger(value = "text-danger"), 8 | Warning(value = "text-warning"), 9 | Info(value = "text-info"), 10 | Light(value = "text-light"), 11 | Dark(value = "text-dark") 12 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/SpinnerVariant.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | sealed class SpinnerVariant(val classes: List = listOf()) { 4 | object Default : SpinnerVariant(listOf("spinner-border")) 5 | object DefaultGrow : SpinnerVariant(listOf("spinner-grow")) 6 | 7 | object Small : SpinnerVariant(listOf("spinner-border", "spinner-border-sm")) 8 | object SmallGrow : SpinnerVariant(listOf("spinner-grow", "spinner-grow-sm")) 9 | 10 | object Large : SpinnerVariant(listOf("spinner-border", "spinner-border-lg")) 11 | object LargeGrow : SpinnerVariant(listOf("spinner-grow", "spinner-grow-lg")) 12 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/ToastPlacement.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | enum class ToastPlacement(val classes: List) { 4 | TopLeft(classes = listOf("top-0", "start-0")), 5 | TopCenter(classes = listOf("top-0", "start-50")), 6 | TopRight(classes = listOf("top-0", "end-0")), 7 | CenterLeft(classes = listOf("top-50", "start-0", "translate-middle-y")), 8 | Center(classes = listOf("top-50", "start-50", "translate-middle")), 9 | CenterRight(classes = listOf("top-50", "end-0", "translate-middle-y")), 10 | BottomLeft(classes = listOf("bottom-0", "start-0")), 11 | BottomCenter(classes = listOf("bottom-0", "start-50", "translate-middle-x")), 12 | BottomRight(classes = listOf("bottom-0", "end-0")) 13 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/ToastStyle.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | enum class ToastStyle(val value: String) { 4 | Primary(value = "text-bg-primary"), 5 | Secondary(value = "text-bg-secondary"), 6 | Success(value = "text-bg-success"), 7 | Danger(value = "text-bg-danger"), 8 | Warning(value = "text-bg-warning"), 9 | Info(value = "text-bg-info"), 10 | Light(value = "text-bg-light"), 11 | Dark(value = "text-bg-dark"), 12 | Link(value = "text-bg-link") 13 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/TooltipDirection.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models 2 | 3 | enum class TooltipDirection(val value: String) { 4 | Top(value = "top"), 5 | Bottom(value = "bottom"), 6 | Left(value = "left"), 7 | Right(value = "right") 8 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/button/ButtonBadge.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models.button 2 | 3 | import com.stevdza.san.kotlinbs.models.BackgroundStyle 4 | import com.stevdza.san.kotlinbs.models.BadgeVariant 5 | import com.varabyte.kobweb.compose.ui.Modifier 6 | 7 | data class ButtonBadge( 8 | val modifier: Modifier = Modifier, 9 | val text: String, 10 | val style: BackgroundStyle = BackgroundStyle.Danger, 11 | val variant: BadgeVariant = BadgeVariant.Rounded 12 | ) 13 | -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/button/ButtonCustomization.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models.button 2 | 3 | import com.stevdza.san.kotlinbs.models.BSBorderRadius 4 | import com.varabyte.kobweb.compose.css.FontWeight 5 | import com.varabyte.kobweb.compose.css.functions.Gradient 6 | import org.jetbrains.compose.web.css.CSSColorValue 7 | import org.jetbrains.compose.web.css.CSSNumeric 8 | import org.jetbrains.compose.web.css.cssRem 9 | import org.jetbrains.compose.web.css.px 10 | 11 | data class ButtonCustomization( 12 | val color: CSSColorValue? = null, 13 | val backgroundColor: CSSColorValue? = null, 14 | val borderColor: CSSColorValue? = null, 15 | val hoverColor: CSSColorValue? = null, 16 | val hoverBackgroundColor: CSSColorValue? = null, 17 | val hoverBorderColor: CSSColorValue? = null, 18 | val activeColor: CSSColorValue? = null, 19 | val activeBackgroundColor: CSSColorValue? = null, 20 | val activeBorderColor: CSSColorValue? = null, 21 | val disabledColor: CSSColorValue? = null, 22 | val disabledBackgroundColor: CSSColorValue? = null, 23 | val disabledBorderColor: CSSColorValue? = null, 24 | 25 | val borderRadius: BSBorderRadius = BSBorderRadius(all = 0.px), 26 | val horizontalPadding: CSSNumeric = 0.75.cssRem, 27 | val verticalPadding: CSSNumeric = 0.375.cssRem, 28 | 29 | val fontFamily: String? = null, 30 | val fontSize: CSSNumeric = 1.cssRem, 31 | val fontWeight: FontWeight = FontWeight.Normal, 32 | val lineHeight: CSSNumeric = 1.5.cssRem, 33 | 34 | val gradient: Gradient? = null 35 | ) 36 | -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/button/ButtonProperty.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models.button 2 | 3 | object ButtonProperty { 4 | const val COLOR = "--bs-btn-color" 5 | const val BACKGROUND_COLOR = "--bs-btn-bg" 6 | const val BORDER_COLOR = "--bs-btn-border-color" 7 | const val HOVER_COLOR = "--bs-btn-hover-color" 8 | const val HOVER_BACKGROUND_COLOR = "--bs-btn-hover-bg" 9 | const val HOVER_BORDER_COLOR = "--bs-btn-hover-border-color" 10 | const val ACTIVE_COLOR = "--bs-btn-active-color" 11 | const val ACTIVE_BACKGROUND_COLOR = "--bs-btn-active-bg" 12 | const val ACTIVE_BORDER_COLOR = "--bs-btn-active-border-color" 13 | const val DISABLED_COLOR = "--bs-btn-disabled-color" 14 | const val DISABLED_BACKGROUND_COLOR = "--bs-btn-disabled-bg" 15 | const val DISABLED_BORDER_COLOR = "--bs-btn-disabled-border-color" 16 | 17 | const val BORDER_RADIUS = "--bs-btn-border-radius" 18 | const val HORIZONTAL_PADDING = "--bs-btn-padding-x" 19 | const val VERTICAL_PADDING = "--bs-btn-padding-y" 20 | 21 | const val FONT_FAMILY = "--bs-btn-font-family" 22 | const val FONT_SIZE = "--bs-btn-font-size" 23 | const val FONT_WEIGHT = "--bs-btn-font-weight" 24 | const val LINE_HEIGHT = "--bs-btn-line-height" 25 | 26 | const val GRADIENT = "background" 27 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/button/ButtonSize.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models.button 2 | 3 | enum class ButtonSize(val value: String) { 4 | Default(value = "btn"), 5 | Small(value = "btn-sm"), 6 | Large(value = "btn-lg") 7 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/button/ButtonType.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models.button 2 | 3 | enum class ButtonType(val value: String) { 4 | Button(value = "button"), 5 | Submit(value = "submit"), 6 | Reset(value = "reset") 7 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/button/ButtonVariant.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models.button 2 | 3 | enum class ButtonVariant(val classes: List) { 4 | Primary(classes = listOf("btn", "btn-primary")), 5 | PrimaryOutline(classes = listOf("btn", "btn-outline-primary")), 6 | Secondary(classes = listOf("btn", "btn-secondary")), 7 | SecondaryOutline(classes = listOf("btn", "btn-outline-secondary")), 8 | Success(classes = listOf("btn", "btn-success")), 9 | SuccessOutline(classes = listOf("btn", "btn-outline-success")), 10 | Danger(classes = listOf("btn", "btn-danger")), 11 | DangerOutline(classes = listOf("btn", "btn-outline-danger")), 12 | Warning(classes = listOf("btn", "btn-warning")), 13 | WarningOutline(classes = listOf("btn", "btn-outline-warning")), 14 | Info(classes = listOf("btn", "btn-info")), 15 | InfoOutline(classes = listOf("btn", "btn-outline-info")), 16 | Light(classes = listOf("btn", "btn-light")), 17 | LightOutline(classes = listOf("btn", "btn-outline-light")), 18 | Dark(classes = listOf("btn", "btn-dark")), 19 | DarkOutline(classes = listOf("btn", "btn-outline-dark")), 20 | Link(classes = listOf("btn", "btn-link")), 21 | LinkOutline(classes = listOf("btn", "btn-outline-link")), 22 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/navbar/NavBarBrand.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models.navbar 2 | 3 | import org.jetbrains.compose.web.css.CSSSizeValue 4 | import org.jetbrains.compose.web.css.CSSUnit 5 | import org.jetbrains.compose.web.css.px 6 | 7 | data class NavBarBrand( 8 | val title: String, 9 | val image: String? = null, 10 | val imageWidth: CSSSizeValue = 30.px, 11 | val href: String 12 | ) 13 | -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/navbar/NavBarButton.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models.navbar 2 | 3 | import com.stevdza.san.kotlinbs.models.button.ButtonBadge 4 | import com.stevdza.san.kotlinbs.models.button.ButtonVariant 5 | 6 | data class NavBarButton( 7 | val id: String? = null, 8 | val text: String, 9 | val variant: ButtonVariant = ButtonVariant.Primary, 10 | val disabled: Boolean = false, 11 | val loading: Boolean = false, 12 | val loadingText: String? = null, 13 | val badge: ButtonBadge? = null, 14 | val onClick: () -> Unit 15 | ) 16 | -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/navbar/NavBarExpand.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models.navbar 2 | 3 | enum class NavBarExpand(val value: String) { 4 | SM(value = "navbar-expand-sm"), 5 | MD(value = "navbar-expand-md"), 6 | LG(value = "navbar-expand-lg"), 7 | XL(value = "navbar-expand-xl"), 8 | XXL(value = "navbar-expand-xxl") 9 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/navbar/NavBarInputField.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models.navbar 2 | 3 | data class NavBarInputField( 4 | val id: String? = null, 5 | val placeholder: String, 6 | val value: String, 7 | val onValueChange: (String) -> Unit, 8 | val onEnterClick: (() -> Unit)? = null, 9 | ) -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/navbar/NavBarOffcanvas.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models.navbar 2 | 3 | import com.stevdza.san.kotlinbs.models.offcanvas.OffcanvasPlacement 4 | 5 | data class NavBarOffcanvas( 6 | val id: String, 7 | val title: String, 8 | val dark: Boolean = false, 9 | val allowScrolling: Boolean = false, 10 | val disableBackdrop: Boolean = false, 11 | val closableOutside: Boolean = true, 12 | val placement: OffcanvasPlacement = OffcanvasPlacement.END 13 | ) 14 | -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/navbar/NavDropdownItem.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models.navbar 2 | 3 | data class NavDropdown( 4 | val placeholder: String, 5 | val items: List 6 | ): NavItem 7 | 8 | data class NavDropdownItem( 9 | val id: String, 10 | val title: String, 11 | val onClick: (Int) -> Unit 12 | ) 13 | -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/navbar/NavItem.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models.navbar 2 | 3 | interface NavItem -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/navbar/NavLink.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models.navbar 2 | 3 | data class NavLink( 4 | val id: String, 5 | val title: String, 6 | val active: Boolean = false, 7 | val disabled: Boolean = false, 8 | val onClick: (Int) -> Unit 9 | ): NavItem -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/offcanvas/OffcanvasBreakpoint.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models.offcanvas 2 | 3 | enum class OffcanvasBreakpoint(val value: String) { 4 | NONE(value = "offcanvas"), 5 | SM(value = "offcanvas-sm"), 6 | MD(value = "offcanvas-md"), 7 | LG(value = "offcanvas-lg"), 8 | XL(value = "offcanvas-xl"), 9 | XXL(value = "offcanvas-xxl"), 10 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/models/offcanvas/OffcanvasPlacement.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.models.offcanvas 2 | 3 | enum class OffcanvasPlacement(val value: String) { 4 | TOP(value = "offcanvas-top"), 5 | BOTTOM(value = "offcanvas-bottom"), 6 | START(value = "offcanvas-start"), 7 | END(value = "offcanvas-end") 8 | } -------------------------------------------------------------------------------- /bootstrap/src/jsMain/kotlin/com/stevdza/san/kotlinbs/util/UniqueIdGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.util 2 | 3 | object UniqueIdGenerator { 4 | private val componentIds = mutableSetOf() 5 | 6 | fun generateUniqueId(baseName: String): String { 7 | var id = baseName 8 | var counter = 1 9 | 10 | while (componentIds.contains(id)) { 11 | id = "$baseName${counter++}" 12 | } 13 | 14 | componentIds.add(id) 15 | return id 16 | } 17 | 18 | fun removeUniqueId(id: String) { 19 | componentIds.remove(id) 20 | } 21 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.multiplatform) apply false 3 | alias(libs.plugins.kobweb.application) apply false 4 | alias(libs.plugins.kobweb.library) apply false 5 | alias(libs.plugins.kobwebx.markdown) apply false 6 | alias(libs.plugins.kotlin.compose) apply false 7 | } 8 | 9 | subprojects { 10 | repositories { 11 | mavenCentral() 12 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 13 | google() 14 | maven("https://us-central1-maven.pkg.dev/varabyte-repos/public") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | jetbrains-compose = "1.7.3" 3 | kobweb = "0.21.1" 4 | kotlin = "2.1.20" 5 | 6 | [libraries] 7 | kobweb-core = { module = "com.varabyte.kobweb:kobweb-core ", version.ref = "kobweb" } 8 | kobweb-compose = { module = "com.varabyte.kobweb:kobweb-compose", version.ref = "kobweb" } 9 | kobweb-silk-core = { module = "com.varabyte.kobweb:kobweb-silk", version.ref = "kobweb" } 10 | kobweb-silk-icons-fa = { module = "com.varabyte.kobwebx:silk-icons-fa", version.ref = "kobweb" } 11 | compose-html-core = { module = "org.jetbrains.compose.html:html-core", version.ref = "jetbrains-compose" } 12 | compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "jetbrains-compose" } 13 | 14 | [plugins] 15 | kobweb-application = { id = "com.varabyte.kobweb.application", version.ref = "kobweb" } 16 | kobwebx-markdown = { id = "com.varabyte.kobwebx.markdown", version.ref = "kobweb" } 17 | kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 18 | kobweb-library = { id = "com.varabyte.kobweb.library", version.ref = "kobweb" } 19 | kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MSYS* | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk11 3 | install: 4 | - echo "Building only the bootstrap library" 5 | - chmod +x gradlew 6 | - ./gradlew :bootstrap:clean :bootstrap:publishToMavenLocal -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | gradlePluginPortal() 6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 7 | maven("https://us-central1-maven.pkg.dev/varabyte-repos/public") 8 | } 9 | } 10 | 11 | rootProject.name = "kotlinbs" 12 | 13 | include(":bootstrap") 14 | include(":site") -------------------------------------------------------------------------------- /site/.gitignore: -------------------------------------------------------------------------------- 1 | # Kobweb ignores 2 | .kobweb/* 3 | !.kobweb/conf.yaml 4 | -------------------------------------------------------------------------------- /site/.kobweb/conf.yaml: -------------------------------------------------------------------------------- 1 | site: 2 | title: "KotlinBootstrap" 3 | 4 | server: 5 | files: 6 | dev: 7 | contentRoot: "build/processedResources/js/main/public" 8 | script: "build/kotlin-webpack/js/developmentExecutable/kotlinbs.js" 9 | prod: 10 | script: "build/kotlin-webpack/js/productionExecutable/kotlinbs.js" 11 | siteRoot: ".kobweb/site" 12 | 13 | port: 8080 -------------------------------------------------------------------------------- /site/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.varabyte.kobweb.gradle.application.util.configAsKobwebApplication 2 | 3 | plugins { 4 | alias(libs.plugins.kotlin.multiplatform) 5 | alias(libs.plugins.kobweb.application) 6 | alias(libs.plugins.kotlin.compose) 7 | } 8 | 9 | group = "com.stevdza.san.kotlinbs" 10 | version = "1.0.0" 11 | 12 | kobweb { 13 | app { 14 | index { 15 | description.set("Powered by Kobweb") 16 | } 17 | } 18 | } 19 | 20 | kotlin { 21 | configAsKobwebApplication("kotlinbs") 22 | 23 | sourceSets { 24 | commonMain.dependencies { 25 | implementation(libs.compose.runtime) 26 | } 27 | 28 | jsMain.dependencies { 29 | implementation(libs.compose.html.core) 30 | implementation(libs.kobweb.core) 31 | implementation(libs.kobweb.silk.core) 32 | implementation(libs.kobweb.silk.icons.fa) 33 | implementation(project(":bootstrap")) 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /site/src/jsMain/kotlin/com/stevdza/san/kotlinbs/MyApp.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs 2 | 3 | import androidx.compose.runtime.* 4 | import com.varabyte.kobweb.compose.ui.modifiers.* 5 | import com.varabyte.kobweb.core.App 6 | import com.varabyte.kobweb.silk.init.InitSilk 7 | import com.varabyte.kobweb.silk.init.InitSilkContext 8 | import com.varabyte.kobweb.silk.SilkApp 9 | import com.varabyte.kobweb.silk.components.layout.Surface 10 | import com.varabyte.kobweb.silk.style.common.SmoothColorStyle 11 | import com.varabyte.kobweb.silk.style.toModifier 12 | 13 | import org.jetbrains.compose.web.css.* 14 | 15 | @InitSilk 16 | fun updateTheme(ctx: InitSilkContext) { 17 | // Configure silk here 18 | } 19 | 20 | @App 21 | @Composable 22 | fun MyApp(content: @Composable () -> Unit) { 23 | SilkApp { 24 | Surface(SmoothColorStyle.toModifier().minHeight(100.vh)) { 25 | content() 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /site/src/jsMain/kotlin/com/stevdza/san/kotlinbs/pages/Index.kt: -------------------------------------------------------------------------------- 1 | package com.stevdza.san.kotlinbs.pages 2 | 3 | import androidx.compose.runtime.* 4 | import com.stevdza.san.kotlinbs.forms.BSSwitch 5 | import com.varabyte.kobweb.compose.foundation.layout.Column 6 | import com.varabyte.kobweb.core.Page 7 | 8 | @Page 9 | @Composable 10 | fun HomePage() { 11 | Column { 12 | var isChecked by remember { mutableStateOf(false) } 13 | 14 | BSSwitch( 15 | label = "Example Switch", 16 | checked = isChecked, 17 | onClick = { 18 | isChecked = it 19 | } 20 | ) 21 | } 22 | } -------------------------------------------------------------------------------- /site/src/jsMain/resources/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevdza-san/KotlinBootstrap/6420d3d92f7cd4d343c02809790f261f403c6bdd/site/src/jsMain/resources/public/favicon.ico --------------------------------------------------------------------------------