Personal patched build of slaveOftime/Fun.Css with PR-equivalent fixes pending upstream merge
  • F# 99.1%
  • Shell 0.9%
Find a file
Ivan The Geek 630f6b6638
All checks were successful
Publish dev package / publish (push) Successful in 43s
Merge fix/nuget-xmldoc-resync: keep shipped XML docs in container builds
2026-05-30 13:46:02 -04:00
.claude chore: align repo with NEXUS agent-files and git hooks 2026-05-29 22:28:05 -04:00
.forgejo/workflows chore: point CI at pinned ms-ubuntu-net10sdk-10.0.300 runner label 2026-05-30 10:43:43 -04:00
.github/workflows chore: CI via host dotnet on net10sdk runner (./build needs absent Docker) 2026-05-29 23:06:13 -04:00
Benchmark chore: adopt NEXUS containerized build via LOGOS template 2026-05-29 22:34:04 -04:00
data/css-spec Phase 4 epilogue: border-width keyword fix + all* migration 2026-05-14 15:00:15 -04:00
Fun.Css chore: adopt NEXUS containerized build via LOGOS template 2026-05-29 22:34:04 -04:00
Fun.Css.Tests test: migrate test deps to Hedgehog 2.0.3 + Verify.Expecto snapshots 2026-05-30 12:10:06 -04:00
LOGOS LOGOS: record cursor modernization commit SHA in upstream PR log 2026-05-14 16:45:00 -04:00
scripts Phase 4 epilogue: border-width keyword fix + all* migration 2026-05-14 15:00:15 -04:00
.build-image chore: adopt NEXUS containerized build via LOGOS template 2026-05-29 22:34:04 -04:00
.gitattributes Add .gitattributes. 2022-05-05 09:34:22 +08:00
.gitignore chore: track pinned global.json for containerized build 2026-05-29 22:35:09 -04:00
AGENTS.md chore: align repo with NEXUS agent-files and git hooks 2026-05-29 22:28:05 -04:00
build fix: stop container builds dropping shipped XML docs 2026-05-30 13:45:45 -04:00
build.fsx feature: finalize test project migration to Expecto-exe (net10) 2026-05-29 23:04:41 -04:00
CHANGELOG.md Add (string) overloads to 47 keyword-shorthand properties (issue #6) 2026-05-12 16:05:34 -04:00
CLAUDE.md chore: align repo with NEXUS agent-files and git hooks 2026-05-29 22:28:05 -04:00
Directory.Build.props chore: adopt NEXUS containerized build via LOGOS template 2026-05-29 22:34:04 -04:00
Fun.Css.sln Init unit test 2024-01-31 12:34:53 +08:00
global.json chore: track pinned global.json for containerized build 2026-05-29 22:35:09 -04:00
LICENSE Create LICENSE 2022-11-22 09:46:01 +08:00
README.md Phase 4.2: README — add "How the bindings are built" section 2026-05-14 01:09:46 -04:00

Fun.Css Nuget

Patched build of slaveOftime/Fun.Css distributed via Forgejo. Tracks upstream master with PR-equivalent fixes pending merge.

Upstream sources (click through to view on GitHub):

See CHANGELOG.md for the patches included on top of upstream.

First, let`s check how it can look like:

style {
    backgroundColor "#44c767"
    borderRadius 30
    borderWidth 1
    borderStyleSolid
    borderColor "#18ab29"
    displayInlineBlock
    cursorPointer
    fontSize 17
}

Benchmarks (I know it is not fair comparison for Fss because Fss is more type safety and will automatically generate classname for you. But I did not find similar libraries to compare, just take as a reference), You can check the code in Benchmark/Benchmarks.fs:

Method Mean Error StdDev Gen 0 Allocated
BuildStyleWithFunCss 181.2 ns 2.33 ns 2.18 ns 0.0343 432 B
BuildStyleWithFunCssCustom 170.9 ns 2.31 ns 2.05 ns 0.0343 432 B
BuildStyleWithFeliz 519.2 ns 8.90 ns 7.89 ns 0.1593 2,000 B
BuildStyleWithFss 6,042.3 ns 65.63 ns 61.39 ns 0.8545 10,736 B

This project is built in Fun.Blazor at first to help build inline style with type safety way.

Before I was using Feliz.Engine, when I was migrating Fun.Blazor to use InlineIfLambda for better performance, I found I can also make style building faster with the same way. So copied the Feliz.Engine basic methods for css and rebuild with computation plus InlineIfLambda.

The basic stuff is like this:

[<CustomOperation("color")>]
member inline _.color([<InlineIfLambda>] comb: CombineKeyValue, color: string) =
    comb &>> ("color", color)

CombineKeyValue is defined as:

type CombineKeyValue = delegate of StringBuilder -> StringBuilder

So after you build with release mode, everything should combined in a local functions with a StringBuilder provide to append all the string pieces together.

How to use it in your project

It depends, take Fun.Blazor as an example, I will just inherit Fun.Css.CssBuilder and add a new Run member to generate the final result. In my case it is a AttrRenderFragment

type StyleBuilder() =
    inherit Fun.Css.CssBuilder()

    member inline _.Run([<InlineIfLambda>] combine: Fun.Css.Internal.CombineKeyValue) =
        AttrRenderFragment(fun _ builder index ->
            let sb = stringBuilderPool.Get()
            builder.AddAttribute(index, "style", combine.Invoke(sb).ToString())
            stringBuilderPool.Return sb
            index + 1
        )

// With a helper function
let style = StyleBuilder()

Then I can use it in Fun.Blazor like this:

div {
    style { 
        color "red"
        height 100
        width 100
    }
}

Another example is just to generate a string for the style, then you can similar do things like:

type StyleStrBuilder() =
    inherit Fun.Css.CssBuilder()

    member inline _.Run([<InlineIfLambda>] combine: Fun.Css.Internal.CombineKeyValue) =
        let sb = stringBuilderPool.Get()
        let str = combine.Invoke(sb).ToString()
        stringBuilderPool.Return sb
        str

// With a helper function
let styleStr = StyleStrBuilder()
For Fable + React, it does not support, because as what I know React is using an js object for the inline style. So the key value is not the pure css standard instead it use camelCase. 
But you can use it in Fable to build pure css inline style string if you want.

How the bindings are built

The CSS surface in this fork is split across two files:

  • Fun.Css/CssBuilder.fs — hand-written: the style { ... } computation expression infrastructure (Builder, Yield, Run, Combine, operators), multi-arg shorthands (margin, padding, flex, gap, transform-origin, box-shadow, …), and CSS function-call value builders (transformMatrix, transformTranslate3D, filterBlur, *CubicBezier, …). Everything that is not a pure single-value property lives here.

  • Fun.Css/CssBuilder.generated.fs — produced by scripts/generate.fsx. One (string) overload plus one typed-keyword op for every Baseline-supported CSS property pulled from W3C webref. Both files compile into the same Fun.Css.CssBuilder type surface.

Data sources

Pinned snapshots under data/css-spec/:

  • webref — W3C's machine-readable CSS spec exports (w3c/webref). One JSON file per spec module (css-fonts-4.json, css-text.json, etc.); 34 modules pinned today.
  • web-features — Baseline / browser-readiness signal (web-platform-dx/web-features). Used to filter out Baseline=Limited properties unless explicitly opted-in.

Commands

# Run the test suite (Expecto + Hedgehog + a hand-rolled snapshot helper)
DOTNET_ROLL_FORWARD=LatestMajor dotnet fsi build.fsx -- -p test

# Regenerate Fun.Css/CssBuilder.generated.fs from the pinned data
DOTNET_ROLL_FORWARD=LatestMajor dotnet fsi scripts/generate.fsx

# Refresh pinned webref + web-features snapshots from upstream
DOTNET_ROLL_FORWARD=LatestMajor dotnet fsi scripts/refresh-data.fsx

# Benchmarks
DOTNET_ROLL_FORWARD=LatestMajor dotnet fsi build.fsx -- -p benchmark

Generator extension model

The generator runs from a small config in scripts/generate.fsx:

Extension Purpose
A Universal *Initial / *InheritFromParent emission for every generated property.
B Per-property int / float overloads (LengthPx adds a px suffix, BareNumber doesn't).
D Per-property keyword override map (extraKeywords) for spec values hidden behind typed-references (<line-style>, <font-stretch-absolute>, <cursor-predefined>, etc.) that the shallow keyword extractor can't see through.
E skipMainOverloads — for properties whose hand-written form has multi-arg overloads (margin, border-width, box-shadow, …), skip the colliding (string)/(int) main but still emit keyword + Ext A surface (distinct op names → no collision).
F The same machinery as E, used to add net-new spec-canonical aliases (displayInline, wordBreak) alongside legacy hand-written names (displayInlineElement, wordbreak).
Filter override allowedDespiteLimited — opt specific properties past the Baseline=Limited exclusion when the property's identity is universal and only narrower values carry the limit (e.g. cursor, background-attachment, text-justify, resize, user-select).

Collisions between hand-written and generator output are handled by name: if the hand-written file already defines cursorAuto, the generator skips emitting its own cursorAuto. This lets the two surfaces coexist while migration is in progress.

Migration map

See LOGOS/upstream-pr-log.md for a per-commit log of what was migrated when, including PR-able boundaries. LOGOS/todos.md is the running task ledger.

TODO

[x] Add css selector, pseudo etc. (help wanted 😊)

But we may not need to build that, because it looks pretty complex and very flexible. Maybe we can just do this:

styleElement {
    ruleset ".selected span:hover" {
        color "red"
    }
}

And it it generate things like

<style>
    .selected span:hover {
        color: red;
    }
</style>

Even there is no type safety for the selector and pseudo class or element, but it is very straightforward to do.