fritz2

Easily build reactive web-apps in Kotlin based on flows and coroutines.

MIT License

Stars
601
Committers
36

Bot releases are hidden (Show)

fritz2 - Version 0.10

Published by WestHuus over 3 years ago

Breaking Changes

PR #334: Rework of API for Keys from KeyboardEvents

The following things has been changed:

  • Using KeyboardEvent.key instead of deprecated KeyboardEvent.keyCode for comparison
  • Extending the list of standard key (e.g. Keys.Alt, Keys.Enter, Keys.Esc)
  • Simplify the creation of a Key by calling the constructor with the given KeyboardEvent
  • Overriden equals() method which compares two Keys by Key.key attribute.
  • The toString() returns the key attribute
input {
    keydowns.key().map { 
        when (it) {
            Keys.Enter -> //...
            Keys.Escape -> //...
            else -> //..
        }
    } handledBy store.myHandler
}

PR #336: Fix horizontal and vertical convenience functions of BordersContext DSL

Swapping the implementations of BorderContext.horizontal(...) and BorderContext.vertical(...) so the borders created by them follow the semantics of the respective method names.

This means: horizontal now creates borders at the top and at the bottom of an element and vertical now creates borders to the left and right of an element.

PR #355: Extends of ColorScheme approach

  • ColorScheme got a function named inverted() it returns a ColorScheme which switches base with highlight and baseContrast with highlightContrast. A common use case might be a creation of inverted theme.
  • The colors info , success, warning, danger and neutral changed to ColorScheme
  • The property color of our button component refactors to type and based on the ColorScheme approach now.
    Our default theme deliveres following predefinitions: primary, secondary, info, success, warning, danger but you can use any other ColorScheme.
     // Until 0.9.x
     clickButton {
         text("danger")
         color { danger }
     }
     
     // New
     clickButton {
         text("danger")
         type { danger }
     }
     clickButton {
         text("custom")
         type { ColorScheme("#00A848", "#2D3748", "#E14F2A", "#2D3748") }
     }
    
  • You've the possibility to setup the button schemes independently of the theme ColorSchemes
    // inside your custom theme implementation
    override val button = object : PushButtonStyles {
        override val types: PushButtonTypes = object : PushButtonTypes {
            override val primary
                get() = ColorScheme(
                    main = "#00A848",
                    mainContrast = "#2D3748",
                    highlight = "#E14F2A",
                    highlightContrast = "#2D3748"
                )
            //...
        }
    }
    
  • inputField and textArea now use background and font color of Colors interface as default.
  • The alert component got a rework by ColorScheme too. Calling the component is the same as before, with the addition that a ColorScheme can now also be passed into it.
    alert {
        content("Severity: success")
        severity { success }
    }
    
    alert {
        content("Severity: custom")
        severity {
            ColorScheme("#00A848", "#2D3748", "#E14F2A", "#2D3748")
        }
    }
    

PR #356: Improve API of AlertComponent and use new ColorScheme

Improves up the AlertComponent API by allowing more flexible customization via the AlertSeverity-style of the theme. Alerts mirroring the severity of a ComponentValidationMessage can still be created via the ComponentValidationMessage.asAlert() extension method. This PR also contains a lot of code cleanup in general.

Alerts now also utilize the new ColorScheme approach for color shades and typography.

The actual usage of the AlertComponent does not change - the underlying styling within the theme does, however.
This means if you are using a custom theme you need to update it in order to implement the altered AlertStyles interface. In case you did not do any modifications to the AlertComponent'-theme your app should continue to work as intended.

  • Updated AlertSeverity interface now includes support for the new ColorScheme class:
    interface AlertSeverity {
        val colorScheme: ColorScheme
        val icon: IconDefinition
    }
    
  • AlertVariants now contains regular styles that are no longer split into styles for the background, text, icons, etc.
    interface AlertVariants {
        val subtle: BasicParams.(AlertSeverity) -> Unit
        val solid: BasicParams.(AlertSeverity) -> Unit
        val leftAccent: BasicParams.(AlertSeverity) -> Unit
        val topAccent: BasicParams.(AlertSeverity) -> Unit
        val discreet: BasicParams.(AlertSeverity) -> Unit
    }
    
  • Removed AlertVariantStyles interface

PR #357: Move background & font colors to Colors interface

Move backgroundColor and fontColor from the Theme interface to the Colors interface.

// before
Theme().fontColor
Theme().backgroundColor

// now
Theme().colors.font
Theme().colors.background

// or in color styling DSL
{  
    background {
        color {
            font //or background
        }
    }
}

New Features

PR #337: Extend styling API

New API for using the fritz2 styling DSL on standard HTML Tags.
Important: add the following import to get the new extension functions: import dev.fritz2.styling.*
The currently approach gets deprecated with v0.10 and will be removed in next major version. So please migrate to the new version as shown below.

// Until 0.9.x
render {
    val textColor = style { color { "yellow" } }
    (::div.styled(baseClass = textColor, id = "hello", prefix = "hello") {
        background { color { "red" } }
    }) {
        +"Hello World!"
    }
}

// New
// don't forget the import dev.fritz2.styling.* !!!
render {
    val textColor = style { color { "yellow" } }
    div({
        background { color { "red" } }
    }, baseClass = textColor, id = "hello", prefix = "hello") {
        +"Hello World!"
    }
}

PR #350 : Add DataTable component

The DataTable component provides a way to visualize tabular data and offers interaction for the user in order to sort by columns or select specific rows.

The API is designed to scale well from the simplest use case of a read only table, towards a fully interactive table with live editing a cell directly within the table itself.

The component is also very flexible and offers lots of customization possibilities like for:

  • the styling of the header or the columns
  • the styling based upon the index or the content of a row or cell
  • the sorting mechanisms (logic and UI)
  • the selection mechanism (logic and UI)

For a detailed overview have a look at our KitchenSink project.

Further new features

  • PR #335 Added missing resizeBehavior option both for textArea component
  • PR #341 moves basicStyles of selectField and textArea to defaultTheme

Improvements

PR #349 Harmonize parameter name in components with stores

For a better readability and intelligibility the parameter store refactores to value or rather values.
value is intended for components with a Store<T> and values is intended for Store<List<T>>.

// Until 0.9.x
 inputField(store = myStore) {}
 checkboxGroup(store = myListStore, items = myItems) {}

// New
inputField(value = myStore) {}
checkboxGroup(values = myListStore, items = myItems) {}

Especially the inputField and textArea get a better intelligibility .

// Unitl 0.9.x
 inputField(store = myStore) {} // Store variant
 inputField { value(myFlow) } // Flow variant

// New
 inputField(value= myStore) {} // Store variant
 inputField { value(myFlow) } // Flow variant

Further improvements

  • PR #340: Moved mono icons to an extra package next to the License.md
  • PR #345: Make staticStyle internal
  • PR #358: Polish DataTable
  • PR #359: Rework new Svg tag in core

Fixed Bugs

  • PR #338: Fixed problem with nested renders which contains conditions
  • PR #351: Fix problem with component-specific EventContexts
fritz2 - Version 0.9.2

Published by haukesomm over 3 years ago

Improvements

  • PR #317: Using websafe fonts in default theme
  • PR #331: Using DSLMarkers for Components

Fixed Bugs

  • PR #319: Suppressing errors messages when inserting normalize.css into CSSStyleSheet

Gradle-Plugin

  • PR #8: Fixed bug when renaming Kotlin multiplatform targets
fritz2 - Version 0.9

Published by jamowei over 3 years ago

Breaking Changes

This release contains changes that break code written with earlier versions.

Global Render Functions (PR#233 & PR#243)

Up until fritz2 version 0.8, we offered two global render functions:

  • render {}: List<Tag<E>> - create RenderContext for multiple root elements
  • renderElement {}: Tag<E> - create RenderContext for single root element
    The result had to be mounted to your DOM-tree by calling, the mount() function.

With version 0.9, we simplified this process by supplying one global render {} function which has no return value, but receives an HTMLElement / selector-String to specify your mount-target (which defaults to the body) as first parameter instead. The mount() function has been removed.

The former global renderElement {} function has been removed as well, since it's executed exactly once and there were no performance gains in using it. Flow<T>.renderElement {} can and should of course still be used when you are sure that you render exactly one root element in it's RenderContext.

See the changes to render functions in the following example code:

// version 0.8
render {
    h1 { +"My App" }
    div(id = "myDiv") {
        store.data.render { value ->
            if (value) div { +"on" } else span { +"off" }
        }
    }
}.mount("target") // mount to ID "target"
// version 0.9
render("#target") { // mount to QuerySelector "#target"
    h1 { +"My App" }
    //div(id = "myDiv") {  // no need to wrap a Flow before rendering
        store.data.render { value ->
            if (value) div { +"on" } else span { +"off" }
        }
    //}
}//.mount("target")

Mounting fritz2-HTML directly to the document.body works without passing the first parameter to the render function because it's the default. With the additional override flag, you can specify whether or not you want to overwrite existing HTML content. This value defaults to true.

Note: A QuerySelecor must be used instead of an id now, which is why you need to start your target string with "#".

Changes in Repositories API (PR#232)

We made some key changes to simplify the usage of fritz2 repositories (localstorage and REST). The Resource class and the ResourceSerializer interface where combined into the new interface Resource, which is now the only interface that needs to be implemented to create a repository. We also renamed the (de-)serialization functions:

  • write -> serialize
  • read -> deserialize
  • writeList -> serializeList - needed only for QueryRepository
  • readList -> deserializeList - needed only for QueryRepository

The following example demonstrates the changes to fritz2 Repositories:

@Lenses
@Serializable
data class ToDo(val id: Long = -1, val text: String = "", val completed: Boolean = false)
// version 0.8
object ToDoSerializer : Serializer<ToDo, String> {
    override fun read(msg: String): ToDo = Json.decodeFromString(ToDo.serializer(), msg)
    override fun readList(msg: String): List<ToDo> = Json.decodeFromString(ListSerializer(ToDo.serializer()), msg)
    override fun write(item: ToDo): String = Json.encodeToString(ToDo.serializer(), item)
    override fun writeList(items: List<ToDo>): String = Json.encodeToString(ListSerializer(ToDo.serializer()), items)
}
val toDoResource = Resource(ToDo::id, ToDoSerializer, ToDo())
val query = restQuery<ToDo, Long, Unit>(toDoResource, "/api/todos")
// version 0.9
object ToDoResource : Resource<ToDo, Long> {
    override val idProvider: IdProvider<ToDo, Long> = ToDo::id
    override fun deserialize(msg: String): ToDo = Json.decodeFromString(ToDo.serializer(), msg)
    override fun deserializeList(msg: String): List<ToDo> = Json.decodeFromString(ListSerializer(ToDo.serializer()), msg)
    override fun serialize(item: ToDo): String = Json.encodeToString(ToDo.serializer(), item)
    override fun serializeList(items: List<ToDo>): String = Json.encodeToString(ListSerializer(ToDo.serializer()), items)
}
val query = restQuery<ToDo, Long, Unit>(ToDoResource, "/api/todos", initialId = -1)

As you can see in this example, it is no longer necessary to supply Resource with a default instance.
Instead, using a REST-Repository requires you to pass initialId to allow the repository to derive from a given instance whether a POST or PUT should be sent. This parameter is not needed for localstorage repositories.

API Streamlining (PR#283)

For the following fritz2 features we streamlined our API a bit:

  • Router
  • Tracking
  • History
  • Validator (note: renamed msgs to data)

They now all have a data: Flow<X> and a current: X property which gives a dynamic Flow<X> or the static current value X of its state.

Components (PR#269 & PR#266)

In order to harmonize our component's API we have changed some fundamental aspects:

  • All component's properties follow a clear semantic: Just pass a value T or a Flow<T> directly as parameter and omit manually wrapping with flowOf:

    // version 0.8
    inputField {
        value { +"Hello world" }
    }
    inputField {
        value { flowOf("Hello world") }
    }
    
    // version 0.9
    inputField {
        value("Hello World")
    }
    
  • the base property for simple form wrapping components like inputField, pushButton and so on has been replaced by the element property.

  • instead of handling events directly within the component's top level configuration context, there is now an events context. So events must now be set up therein.

  • the items for all grouping forms have to be passed now directly as function parameter instead of setting it within the configuration context:

    // version 0.8
    checkboxGroup {
        items { items= listOf("A", "B", "C") }
    }
    
      // version 0.9
    checkboxGroup(items= listOf("A", "B", "C")) {
    }
    

    The following component's are affected by this:

    • checkboxGroup
    • radioGroup
    • selectField (new component)

new features

  • added window event-listenersPR#277
  • added AppFrame and navigation-components PR#262
  • added file selector component PR#258
  • support for JS IR compiler PR#254
  • allow important to CSS-properties in DSL PR#248
  • components validation PR#244

improvements

  • harmonized toast behavior PR#285
  • small refactoring for Router PR#281
  • added convenience function to register WebComponent PR#279
  • opened component's stores for extension PR#276
  • improved CloseButton API PR#275
  • small improvements to FormControl PR#272
  • added convenience functions for Lenses PR#271
  • rework element property prioritization PR#268
  • added convenience methods for Validator PR#242

fixed bugs

  • fixed default styling of selectField PR#274
  • fixed placement of modals PR#249
  • adjusted WebComponents to new Render/TagContext PR#247
fritz2 - Version 0.8

Published by jwstegemann almost 4 years ago

breaking changes

This release contains changes that break code written with earlier versions. Hopefully these are the last major api-changes prior to fritz2 1.0:

Setting attributes per function

In fritz2 0.8 we decided to use functions to set attribute values instead of vars with delegation.
That way you do not have to wrap constant values in a Flow anymore. This yields better performance and the const()-function could be removed. For convenience reasons we also added a new function asString for Flows to
convert a Flow to a Flow<String> by calling the toString() method internally.

input {
    type("text") // native
    value(myStore.data) // flow
    name(otherStore.data.asString()) // otherStore.data is not a Flow of String
}

RenderContext replaces HtmlElements

We renamed the HtmlElements interface to RenderContext, because we think this name better fits the Kotlin DSL approach.
The idea behind it is that every render function creates a new RenderContext in which
new Tags can be created. This also means that you must replace the receiver type in your custom component-functions accordingly:

val errorStore = storeOf("some text")

// own component
fun RenderContext.errorText(text: String): P {
    return p("error") {
        +text
    }
}

errorStore.data.render { //this: RenderContext
    errorText(it)
}

Adding Text and Comments

We clarified the creation of TextNodes in Tags. Now you use unary +-operator for constant Strings
to append text at this position to your Tag. If you have a Flow, call asText() instead.
To create a CommentNode, you can use the !-operator and asComment() analogous. This intentionally follows a different approach in contrast to the attribute functions so it can be distinguished more easily.

p {
    +"Hello "
    myStore.data.asText()

    !"this is a comment"
    myStore.data.asComment()
}

Evolution of render() and renderEach()

Using former fritz2-versions you mapped a Flow of data to a Flow of Tags and created a MountPoint explicitly by calling bind() at some place in your rendering. This was error prone. Since nobody would do anything with a Flow<Tag> other than binding it, all render functions now implicitly create the mount point and therefore no bind() is necessary anymore. It has been removed completely.

val myStore = storeOf(listOf("a","b","c"))

render {
    ul {
    	myStore.data.renderEach {
    		li { +it }
    	} // no .bind() here anymore
    }
}

For performance reasons the render-functions prior to version 0.8 did not allow more than one root-element. In version 0.8 the standard render allows you to add as many root elements to your context as you want or even none:

val myStore = storeOf(42)

// renders multiple root-elements
myStore.data.render {
	repeat(it) {
		div { +"one more" }
	}
}

// does only render something if value is large enough
myStore.data.render {
	if (it > 100) {
		div { +"number" }
	}
}

If you you do not need this feature (because you know you will always have exactly one root-element) use renderElement() instead to get (slightly) improved performance.

render() and renderElement() now reserve their place in the DOM until the content is rendered by using a temporary placeholder. Since this costs some performance you can disable it when you are sure that there are no sibling-elements on the same level in your DOM-tree by setting renderElement(preserveOrder = false). Use this when you have to render lots of elements (in huge lists, tables, etc.).

Instead of someListFlow.each().render {...}.bind() you now simply write someListFlow.renderEach {...}. This is analog for all flavors of renderEach on Stores and Flows with and without an idProvider.
Please note that renderEach() still allows only one root-element (like renderElement)!

Tracker offers Flow<Boolean>

Tracker now implements Flow<Boolean> instead of Flow<String?>so it adopts better to most use-cases. Find an example here.

new features

improvements

  • update all dependencies to latest version PR#166
  • extend Router functionality PR#197
  • upgraded Dokka-version and moved to html for api-docs PR#194
  • annotation processor visibility option PR#178
  • use local test server PR#165

fixed bugs

  • fix memory leaks and performance issues PR#180 PR#185
  • no trailing slash in remote PR#167
  • fix boolean attribute delegates PR#172
fritz2 - Version 0.7.2

Published by jwstegemann about 4 years ago

Small patch resolving a memory issue related to coroutine scopes.

fritz2 - Version 0.7.1

Published by jwstegemann about 4 years ago

Just a small patch to be compatible with Kotlin 1.4.0.

No new features or bug fixes included.

fritz2 - Version 0.7

Published by jwstegemann about 4 years ago

breaking changes

This release contains changes that break code written with earlier versions:

  • Handlers are now suspendable, so you can call suspend-methods directly inside your Handler. There is no need for Applicator anymore. Therefore this class and its utility-functions have been removed. (PR#124 & PR#126)
  • FormateStore and interface Format have been removed. Use format-factory-function inside lenses package to create a formatting Lens and create a normal SubStore (by using sub). (PR#139 & PR#146)
val df: DateFormat = DateFormat("yyyy-MM-dd")
// converts a Date into String in vice versa
val dateFormat = format(
    parse = { df.parseDate(it) },
    format = { df.format(it) }
)

//using the dateLens
val birthday = personStore.sub(L.Person.birthday + dateFormat)
// or
val birthday = personStore.sub(L.Person.birthday).sub(dateFormat)
  • Validation has been extracted as a service and refactored to be more concise. (PR#149 & #157)

in commonMain

data class Message(val id: String, val status: Status, val text: String) : ValidationMessage {
    override fun isError(): Boolean = status > Status.Valid // renamed from failed() -> isError()
}

object PersonValidator : Validator<Person, Message, String>() {
   // return your validation messages here
   override fun validate(data: Person, metadata: String): List<Message> {
       ...
   }
}

in jsMain

val personStore = object : RootStore<Person>(Person()) {    
    // only update when it's valid
    val addOrUpdate = handle<Person> { oldPerson, newPerson ->
        if (PersonValidator.isValid(newPerson, "update")) new else oldPerson
    }
}
...

// then render the validation message list in your html
PersonValidator.msgs.render { msg ->
    ...
}.bind()

in jvmMain

if (PersonValidator.isValid(newPerson , "add")) {
    //e.g. save your new Person to Database
    ...
} else {
   // get the messages, only available after isValid() was called
   val msgs = PersonValidator.msgs
   ...
}

new features

  • added tracking-service to access process state of Handlers (e.g. to show process indicator). (PR#147)
  • added history-service to keep track of historical values in Stores and provide back() function. (PR#152)
  • added Repository to offer CRUD-functionality for entities and dealing with queries. Implementations are available for REST and LocalStorage (see example). (PR#141, PR#144, PR#155 & PR#153)
  • added storeOf() function to create a minimal RootStore (without Handlers) (PR#144)
  • added convenience-function render on Seq, so you can directly write each(...).render { ... } (and leave out map) (PR#142)
  • added convenience-function render on Flow, so you can directly write flow.render { ... } (and leave out map) (PR#154)
  • added functions to deal with errors in Handlers (PR#137)
  • snapshots are now provided on oss.jfrog.org (PR#128)
  • added append function to remote (PR#127)
  • changed IdProvider to generic type (PR#123)
  • use Inspector (created by inspect()-function) to navigate through your model in validation and test and have data and corresponding ids available at any point (PR#118)

fixed bugs

  • added isValid on JVM (PR#135)
  • added missing factories for <dt> and <dd> (PR#134)
  • added missing SelectedAttributeDelegate (PR#131)
  • fixed some bugs in Router and minor API changes (PR#151)
fritz2 - Version 0.6

Published by jwstegemann over 4 years ago

breaking changes

This release contains changes that break code written with earlier versions:

  • You no longer need to inherit WithId in your model-classes (the interface has been removed from fritz2 entirely). Instead, you need to provide a function which returns the id of a certain instance. This function can be used when calling each or creating a SubStore for a certain element (PR#94):
// in commonMain
@Lenses
data class Model(val id: String, val value: String)

// in jsMain
val store = RootStore<List<Model>>(listOf(...))

render {
  ul {
    store.each(Model::id).map { modelStore ->
      render {
        li { modelStore.sub(L.Model.value).data.bind() }
      }
    }.bind()
  }
}.mount("target")
  • All of the each methods (PR#113) were unified:

    • use Flow<T>.each() to map each instance of T to your Tags. It uses Kotlin's equality function to determine whether or not two elements are the same, and therefore re-renders the whole content you mapped when an element is changed or moved.

    • with Flow<T>.each(idProvider: (T) -> String) you can also map each instance of T to your Tags, but it uses the given idProvider to determine whether or not two elements are the same. In your mapping, you can get a SubStore for an element using listStore.sub(id, idProvider), so only the parts that actually changed will be re-rendered.

    • use Store<List<T>>.each() to map a SubStore<T> to Tags. It uses the list position of the element to determine whether or not two elements are the same. This means that when inserting something into the middle of the list, the changed element AND ALL following elements will be re-rendered.

    • with Store<List<T>>.each(idProvider: (T) -> String) you can also map a SubStore<T> to Tags, but it uses the given idProvider to determine whether or not two elements are the same`, so only the parts that actually changed will be re-rendered.

    • renamed handleAndEmit to handleAndOffer (PR#109)

    • renamed ModelIdRoot to RootModelId to follow the naming of stores (PR#96)

new features

  • add static text in HTML by +"yourText" (PR#95)
  • add HTML-comments by comment("yourText") or !"yourText" (PR#108)
  • use the action function to dispatch an action at any point in your code (PR#117)

fixed bugs

  • fixed handling of value and checked attributes (PR#81)
  • fixed MapRouter to use Map<String,String> (PR#82)
  • fixed double kotlin-block in gradle build-file (PR#97)
  • ensure order of children when mixing static tags with bound values on the same level by using bind(preserveOrder = true) (PR#102)
  • classes of HTML-tags are now open so you can inherit your own tags from them (PR#104)
  • SingleMountPoint for Boolean (leaving out the attribute if false) (PR#105)
fritz2 - Version 0.5

Published by jwstegemann over 4 years ago

breaking changes

This release contains changes, that break code written with earlier versions:

  • We moved all artifacts and packages to match our domain: dev.fritz2. You will have to adjust your inputs and dependencies accordingly.
  • The default project-type for fritz2 now is multiplatform (to make it easy to share models and validation between client and server). Use the new fritz2-gradle-plugin to setup your project:

build.gradle.kts

plugins {
    id("dev.fritz2.fritz2-gradle") version "0.5"
}

repositories {
    jcenter()
}

kotlin {
    kotlin {
        jvm()
        js().browser()

        sourceSets {
            val commonMain by getting {
                dependencies {
                    implementation(kotlin("stdlib"))
                }
            }
            val jvmMain by getting {
                dependencies {
                }
            }
            val jsMain by getting {
                dependencies {
                }
            }
        }
    }
}

fixed bugs

  • fixed dom-update problem with checked attribute at HTMLInputElement
fritz2 - Version 0.4

Published by jwstegemann over 4 years ago

breaking changes

This release contains changes, that break code written with earlier versions:

  • since it was the source of much confusion we renamed the function to build a tree of Tags (formerly html) to render:
render {
    div("my-class") {
        // ...
    }
}
  • the overloaded operator <= to bind a Flow of actions or events to a Handler was definitely not Kotlin-like, so we replaced it by the handledBy infix-function (please note the reversed order):
button("btn btn-primary") {
    text("Add a dot")
    clicks handledBy store.addADot
}

new features

  • improved remote-api
  • support for building and using WebComponents

bug fixes

  • improved examples
  • improved documentation

build.gradle.kts

Kotlin style

dependencies {
    implementation("io.fritz2:fritz2-core-js:0.4")
}

Groovy style

dependencies {
    implementation 'io.fritz2:fritz2-core-js:0.4'
}
fritz2 - Version 0.3

Published by jwstegemann over 4 years ago

  • several bug-fixes
  • tidyed up syntax for text, attr, const
  • better examples
  • Improved diff-algorithm for list-handling
  • better extractions on events (current value, selected item, etc.)
  • reworked structure of GitHub-projects
fritz2 - Version 0.2

Published by jwstegemann over 4 years ago

First automated release using github actions...

fritz2 - Version 0.1

Published by jwstegemann over 4 years ago

Our first public release, just to test the build- and publish-process.