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.
Published on the Elm package registry: package.elm-lang.org/packages/alexbruf/elm-cally/1.1.0
elm install alexbruf/elm-callyModule 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.
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.
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.
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.
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.