alexbruf/elm-cally/1.1.0

elm-cally

A pure-Elm port of Cally, the small, dependency-light calendar web components. No JavaScript, no ports, no Intl, no shadow DOM -- elm-cally renders the same markup and the same part attributes as the JS version with Html.node, so it can be styled with (a light-DOM rewrite of) Cally's own CSS.

Three pickers are provided: Cally.Date (single date), Cally.Range (date range) and Cally.Multi (multiple dates), each built from a shared Cally.Month grid. Cally's test suite is the specification: every upstream test is either ported (see tests/) or explicitly skipped with a reason, and upstream/ported-tests.json / check-coverage.mjs enforce that mapping in CI.

Install

Published on the Elm package registry: package.elm-lang.org/packages/alexbruf/elm-cally/1.1.0

elm install alexbruf/elm-cally

Module docs for every exposed module live on that page. For the default look, add the stylesheet: Cally.Css.stylesheet ships it inside the package (see Styling), and cally.css at the root of this repo is the same bytes as a file.

Usage

module Main exposing (main)

import Browser
import Cally.Date as Date
import Cally.Locale as Locale
import Cally.Month as Month
import Date
import Html exposing (Html)


type alias Model =
    { value : Date.Value
    , picker : Date.Model
    }


type Msg
    = PickerMsg Date.Msg
    | Changed Date.Value


config : Date.Config Msg
config =
    Date.defaultConfig
        { id = "my-date-picker"
        , today = today -- a `Date.Date` from `elm/time` + `justinmimbs/date`
        , locale = Locale.enGB
        , toMsg = PickerMsg
        , onChange = Changed
        }


init : Model
init =
    { value = Nothing
    , picker = Date.init config Nothing
    }


view : Model -> Html Msg
view model =
    Date.view config
        model.value
        model.picker
        [ Month.view { offset = 0 } ]


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        PickerMsg subMsg ->
            let
                ( picker, cmd ) =
                    Date.update config model.value subMsg model.picker
            in
            ( { model | picker = picker }, Cmd.map PickerMsg cmd )

        Changed value ->
            ( { model | value = value }, Cmd.none )

Cally.Month.view (and Cally.SelectYear.view / Cally.SelectMonth.view) take a Context msg, which Date.view / Range.view / Multi.view build and pass to each child function -- that's how Elm reproduces Cally's light-DOM slotting: children are just functions of Context, so they can be placed in any order, including inside the heading slot for a custom year/month picker. See CLAUDE.md for the full API contract and example/ for range, multi, multi-month, week-number and RTL usage.

Styling

The default look is one stylesheet. Since 1.1.0 it ships inside the package as Cally.Css.stylesheet, a plain String, because the Elm package registry only distributes src/, elm.json, README.md and LICENSE -- a file at the repository root can never reach you through elm install.

Put it on the page with a <style> node:

import Cally.Css
import Html exposing (Html, node, text)

view : Model -> Html Msg
view model =
    Html.div []
        [ node "style" [] [ text Cally.Css.stylesheet ]
        , Cally.Date.view config model.value model.picker [ Month.view { offset = 0 } ]
        ]

The rules are global (they target element names and part attributes), so render that node once, anywhere in the tree. Cally.Css.version is the package version the string was generated from -- a useful cache key if a build step writes it out to a real .css file instead.

cally.css at the repository root is the same bytes, kept in sync by scripts/gen-css-module.mjs and checked in CI, so a build pipeline that prefers a file can still copy it (that is what example/ does).

The stylesheet is a translation of Cally's own shadow-DOM stylesheet into plain CSS: no shadow DOM means no ::part(), so :host becomes the element name and ::part(x) becomes the attribute selector [part~="x"], which matches space-separated part tokens exactly the way ::part() does. If you already have Cally-targeting CSS, the rewrite is mechanical:

/* before (targets the Cally web component) */
calendar-date::part(day selected) {
  background: hotpink;
}

/* after (targets elm-cally's light-DOM markup) */
calendar-date [part~="day"][part~="selected"] {
  background: hotpink;
}

See docs/migrate-css.md for the full rule, a sed/perl regex that performs it, and more examples.

Development

bun install                      # root dev tooling (elm-test, elm-review, elm-format, playwright)
cd upstream && npm ci            # pins the upstream `cally` version (Dependabot-owned)
node upstream/scripts/fetch-upstream-src.mjs   # clones cally's source at the pinned tag

bunx elm-test                    # Layer 1: elm-test (Test.Html.Query/Event)
bunx elm-review                  # jfmengels standard config
bunx elm-format --validate src tests example/src
bunx elm make --docs=docs.json
bun run css:generate                             # cally.css -> src/Cally/Css.elm (committed)
bun run css:check                                # fail if src/Cally/Css.elm is stale

node upstream/scripts/list-upstream-tests.mjs    # print every upstream describe/it title
node upstream/scripts/generate-ported-tests.mjs  # regenerate upstream/ported-tests.json from tests/
node upstream/scripts/check-coverage.mjs         # fail if any upstream title is unmapped
node upstream/scripts/capture-parts.mjs          # regenerate upstream/parts-fixture.json via real Cally in Playwright

bunx playwright test             # Layer 2: tests-browser/, against example/

Use bun/bunx, not npm/npx, everywhere except inside upstream/, which is a separate npm project so Dependabot can bump the pinned cally version on its own.

See CLAUDE.md for the full working contract (upstream facts, API contract, DOM ids, behaviour rules, test conventions) and SPEC.md for the original brief.

Attribution

elm-cally is a from-scratch Elm implementation, ported test-for-test and markup-for-markup from Cally by Nick Williams and contributors, MIT licensed. No Cally source code is vendored or redistributed; the upstream repository is cloned into upstream/.cache/ (gitignored) only as a reference for the port and its tests.

elm-cally itself is MIT licensed -- see LICENSE.